From dfd29c8e6eb65d1265d4e807677d96b22f8511b4 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Sun, 11 Jul 2021 18:14:45 -0600 Subject: [PATCH 01/56] Fix timestamped pipelines --- .../com/github/serivesmejia/eocvsim/EOCVSim.kt | 2 +- .../serivesmejia/eocvsim/input/InputSource.java | 2 ++ .../eocvsim/input/source/CameraSource.java | 9 +++++++++ .../eocvsim/input/source/ImageSource.java | 5 +++++ .../eocvsim/input/source/VideoSource.java | 8 ++++++++ .../eocvsim/pipeline/PipelineManager.kt | 2 +- .../easyopencv/TimestampedPipelineHandler.kt | 10 +++------- .../ftc/teamcode/TimestampedPipelineTest.kt | 16 ++++++++++++++++ 8 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TimestampedPipelineTest.kt diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt index 751f1333..287a0136 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt @@ -232,7 +232,7 @@ class EOCVSim(val params: Parameters = Parameters()) { } fun destroy(reason: DestroyReason) { - Log.warn(TAG, "Destroying current EOCVSim ($hexCode) due to $reason") + Log.warn(TAG, "Destroying current EOCVSim ($hexCode) due to $reason, it is normal to see InterruptedExceptions and other kinds of stack traces blow") //stop recording session if there's currently an ongoing one currentRecordingSession?.stopRecordingSession() diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.java index 9ceec0e3..b71fec57 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.java @@ -80,6 +80,8 @@ public final String getName() { public abstract FileFilter getFileFilters(); + public abstract long getCaptureTimeNanos(); + @Override public final int compareTo(InputSource source) { return createdOn > source.createdOn ? 1 : -1; diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.java index f7f7d77f..cf50f472 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.java @@ -31,6 +31,7 @@ import org.opencv.core.Size; import org.opencv.imgproc.Imgproc; import org.opencv.videoio.VideoCapture; +import org.opencv.videoio.Videoio; import org.openftc.easyopencv.MatRecycler; import javax.swing.filechooser.FileFilter; @@ -51,6 +52,8 @@ public class CameraSource extends InputSource { private volatile transient MatRecycler matRecycler; + private transient long capTimeNanos = 0; + public CameraSource(int webcamIndex, Size size) { this.webcamIndex = webcamIndex; this.size = size; @@ -126,6 +129,7 @@ public Mat update() { MatRecycler.RecyclableMat newFrame = matRecycler.takeMat(); camera.read(newFrame); + capTimeNanos = System.nanoTime(); if (newFrame.empty()) { newFrame.returnMat(); @@ -184,6 +188,11 @@ public FileFilter getFileFilters() { return null; } + @Override + public long getCaptureTimeNanos() { + return capTimeNanos; + } + @Override public String toString() { if (size == null) size = new Size(); diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java index a92a1298..7d605980 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java @@ -164,6 +164,11 @@ public FileFilter getFileFilters() { return FileFilters.imagesFilter; } + @Override + public long getCaptureTimeNanos() { + return System.nanoTime(); + } + @Override public String toString() { if (size == null) size = new Size(); diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java index 00633da4..35c70c7d 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java @@ -57,6 +57,8 @@ public class VideoSource extends InputSource { private transient double lastFramePosition = 0; + private transient long capTimeNanos = 0; + public VideoSource(String videoPath, Size size) { this.videoPath = videoPath; this.size = size; @@ -143,6 +145,7 @@ public Mat update() { MatRecycler.RecyclableMat newFrame = matRecycler.takeMat(); video.read(newFrame); + capTimeNanos = System.nanoTime(); //with videocapture for video files, when an empty mat is returned //the most likely reason is that the video ended, so we set the @@ -202,6 +205,11 @@ public FileFilter getFileFilters() { return FileFilters.videoMediaFilter; } + @Override + public long getCaptureTimeNanos() { + return capTimeNanos; + } + @Override public String toString() { return "VideoSource(" + videoPath + ", " + (size != null ? size.toString() : "null") + ")"; diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt index e0c3b237..5ec974ce 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt @@ -189,7 +189,7 @@ class PipelineManager(var eocvSim: EOCVSim) { return } - timestampedPipelineHandler.update(currentPipeline) + timestampedPipelineHandler.update(currentPipeline, eocvSim.inputSourceManager.currentInputSource) lastPipelineAction = if(!hasInitCurrentPipeline) { "init/processFrame" diff --git a/EOCV-Sim/src/main/java/org/openftc/easyopencv/TimestampedPipelineHandler.kt b/EOCV-Sim/src/main/java/org/openftc/easyopencv/TimestampedPipelineHandler.kt index 0524d8ed..3cc94c9a 100644 --- a/EOCV-Sim/src/main/java/org/openftc/easyopencv/TimestampedPipelineHandler.kt +++ b/EOCV-Sim/src/main/java/org/openftc/easyopencv/TimestampedPipelineHandler.kt @@ -23,18 +23,14 @@ package org.openftc.easyopencv -import com.qualcomm.robotcore.util.ElapsedTime +import com.github.serivesmejia.eocvsim.input.InputSource class TimestampedPipelineHandler { - private val elapsedTime = ElapsedTime() - - fun update(currentPipeline: OpenCvPipeline?) { + fun update(currentPipeline: OpenCvPipeline?, currentInputSource: InputSource?) { if(currentPipeline is TimestampedOpenCvPipeline) { - currentPipeline.setTimestamp(elapsedTime.nanoseconds()) + currentPipeline.setTimestamp(currentInputSource?.captureTimeNanos ?: 0L) } - - elapsedTime.reset() } } \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TimestampedPipelineTest.kt b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TimestampedPipelineTest.kt new file mode 100644 index 00000000..1eb011b5 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TimestampedPipelineTest.kt @@ -0,0 +1,16 @@ +package org.firstinspires.ftc.teamcode + +import org.firstinspires.ftc.robotcore.external.Telemetry +import org.opencv.core.Mat +import org.openftc.easyopencv.TimestampedOpenCvPipeline + +class TimestampedPipelineTest(val telemetry: Telemetry) : TimestampedOpenCvPipeline() { + + override fun processFrame(input: Mat, captureTimeNanos: Long): Mat { + telemetry.addData("cap time nanos", captureTimeNanos) + telemetry.update() + + return input + } + +} \ No newline at end of file From a0bca1ae5dd7e49608748c15f64b41cbafa546a2 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Sat, 28 Aug 2021 13:12:12 -0600 Subject: [PATCH 02/56] Before deleting PackageStatementRemover --- .../compiler/CompiledPipelineManager.kt | 10 ++- .../pipeline/compiler/PipelineClassLoader.kt | 4 +- .../compiler/util/PackageStatementRemover.kt | 76 +++++++++++++++++++ .../workspace/config/WorkspaceConfig.java | 2 + .../ftc/teamcode/TimestampedPipelineTest.kt | 16 ---- build.gradle | 2 +- 6 files changed, 89 insertions(+), 21 deletions(-) create mode 100644 EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/util/PackageStatementRemover.kt delete mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TimestampedPipelineTest.kt diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt index d90bc353..fff04b60 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt @@ -27,6 +27,7 @@ import com.github.serivesmejia.eocvsim.gui.DialogFactory import com.github.serivesmejia.eocvsim.gui.dialog.Output import com.github.serivesmejia.eocvsim.pipeline.PipelineManager import com.github.serivesmejia.eocvsim.pipeline.PipelineSource +import com.github.serivesmejia.eocvsim.pipeline.compiler.util.PackageStatementRemover import com.github.serivesmejia.eocvsim.util.Log import com.github.serivesmejia.eocvsim.util.StrUtil import com.github.serivesmejia.eocvsim.util.SysUtil @@ -52,6 +53,7 @@ class CompiledPipelineManager(private val pipelineManager: PipelineManager) { val SOURCES_OUTPUT_FOLDER = File(COMPILER_FOLDER, File.separator + "gen_src").mkdirLazy() val CLASSES_OUTPUT_FOLDER = File(COMPILER_FOLDER, File.separator + "out_classes").mkdirLazy() val JARS_OUTPUT_FOLDER = File(COMPILER_FOLDER, File.separator + "out_jars").mkdirLazy() + val FLAT_SRC_FOLDER = File(COMPILER_FOLDER, File.separator + "flat_src").mkdirLazy() val PIPELINES_OUTPUT_JAR = File(JARS_OUTPUT_FOLDER, File.separator + "pipelines.jar") @@ -115,8 +117,12 @@ class CompiledPipelineManager(private val pipelineManager: PipelineManager) { val runtime = ElapsedTime() + val (files, sourcesPath) = if(workspaceManager.workspaceConfig.ignorePackages) { + PackageStatementRemover.processFiles(workspaceManager.sourceFiles) + } else Pair(workspaceManager.sourceFiles, absoluteSourcesPath) + val compiler = PipelineCompiler( - absoluteSourcesPath, workspaceManager.sourceFiles, + sourcesPath, files, workspaceManager.resourcesAbsolutePath.toFile(), workspaceManager.resourceFiles ) @@ -201,7 +207,7 @@ class CompiledPipelineManager(private val pipelineManager: PipelineManager) { fun compile(fixSelectedPipeline: Boolean = true) = try { runBlocking { uncheckedCompile(fixSelectedPipeline) } - } catch(e: Exception) { + } catch(e: Throwable) { isBuildRunning = false onBuildEnd.run() diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineClassLoader.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineClassLoader.kt index 327da7bf..40ff6fa2 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineClassLoader.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineClassLoader.kt @@ -56,7 +56,7 @@ class PipelineClassLoader(pipelinesJar: File) : ClassLoader() { } private fun loadClass(entry: ZipEntry): Class<*> { - val name = entry.name.removeFromEnd(".class").replace(File.pathSeparatorChar, '.') + val name = entry.name.removeFromEnd(".class").replace(File.separatorChar, '.') zipFile.getInputStream(entry).use { inStream -> ByteArrayOutputStream().use { outStream -> @@ -73,7 +73,7 @@ class PipelineClassLoader(pipelinesJar: File) : ClassLoader() { if(clazz == null) { try { - clazz = loadClass(zipFile.getEntry(name.replace('.', File.pathSeparatorChar) + ".class")) + clazz = loadClass(zipFile.getEntry(name.replace('.', File.separatorChar) + ".class")) if(resolve) resolveClass(clazz) } catch(e: Exception) { clazz = super.loadClass(name, resolve) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/util/PackageStatementRemover.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/util/PackageStatementRemover.kt new file mode 100644 index 00000000..88c09754 --- /dev/null +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/util/PackageStatementRemover.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.pipeline.compiler.util + +import com.github.serivesmejia.eocvsim.pipeline.compiler.CompiledPipelineManager +import com.github.serivesmejia.eocvsim.util.SysUtil +import com.github.serivesmejia.eocvsim.util.extension.zeroBased +import java.io.File + +object PackageStatementRemover { + + fun processFiles(sourceFiles: List): Pair, File> { + val files = arrayListOf() + + SysUtil.deleteFilesUnder(CompiledPipelineManager.FLAT_SRC_FOLDER) + + for(file in sourceFiles) { + val destination = File(CompiledPipelineManager.FLAT_SRC_FOLDER.absolutePath + File.separator + file.name) + if(destination.exists()) { + throw IllegalStateException("Duplicate class file, name: ${file.name}. Make sure all the files in all the packages have a unique name") + } + + SysUtil.saveFileStr( + destination, + stripPackageDeclaration( + SysUtil.loadFileStr(file) + ) + ) + + files.add(destination) + } + + return Pair(files, CompiledPipelineManager.FLAT_SRC_FOLDER) + } + + val PACKAGE_DECLARATION_REGEX = Regex("package ([\\w&&\\D||\\n]([\\w\\.]*[\\w])?);") + + fun stripPackageDeclaration(src: String): String { + val match = PACKAGE_DECLARATION_REGEX.find(src) + + return if(match != null) { + val builder = StringBuilder() + val newLines = match.value.lines().size - 1 + + builder.append(src.subSequence(0, match.range.first)) + repeat(newLines) { + builder.appendLine() + } + builder.append(src.subSequence(match.range.last + 1, src.length.zeroBased)) + + builder.toString() + } else src + } + +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfig.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfig.java index 061abb8f..c21e1959 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfig.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfig.java @@ -27,6 +27,8 @@ public class WorkspaceConfig { + public boolean ignorePackages = true; + public String sourcesPath = "."; public String resourcesPath = "."; diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TimestampedPipelineTest.kt b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TimestampedPipelineTest.kt deleted file mode 100644 index 1eb011b5..00000000 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TimestampedPipelineTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.firstinspires.ftc.teamcode - -import org.firstinspires.ftc.robotcore.external.Telemetry -import org.opencv.core.Mat -import org.openftc.easyopencv.TimestampedOpenCvPipeline - -class TimestampedPipelineTest(val telemetry: Telemetry) : TimestampedOpenCvPipeline() { - - override fun processFrame(input: Mat, captureTimeNanos: Long): Mat { - telemetry.addData("cap time nanos", captureTimeNanos) - telemetry.update() - - return input - } - -} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 22c9ceac..d125dc5a 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ buildscript { allprojects { group 'com.github.serivesmejia' - version '3.1.0' + version '3.2.0' ext { standardVersion = version From 56354957d70b497ab81e8d3f4c4a63dfd1a1718f Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Sat, 28 Aug 2021 19:10:21 -0600 Subject: [PATCH 03/56] Add command arguments with picocli --- EOCV-Sim/build.gradle | 1 + .../com/github/serivesmejia/eocvsim/Main.kt | 29 ++++++- .../compiler/CompiledPipelineManager.kt | 11 +-- .../compiler/util/PackageStatementRemover.kt | 76 ------------------- .../workspace/config/WorkspaceConfig.java | 2 - 5 files changed, 30 insertions(+), 89 deletions(-) delete mode 100644 EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/util/PackageStatementRemover.kt diff --git a/EOCV-Sim/build.gradle b/EOCV-Sim/build.gradle index 085973c8..b79d0e23 100644 --- a/EOCV-Sim/build.gradle +++ b/EOCV-Sim/build.gradle @@ -39,6 +39,7 @@ dependencies { implementation 'org.openpnp:opencv:4.3.0-2' implementation 'com.github.sarxos:webcam-capture:0.3.12' + implementation 'info.picocli:picocli:4.6.1' implementation 'com.google.code.gson:gson:2.8.7' implementation 'io.github.classgraph:classgraph:4.8.108' diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt index a62c9a79..2dd20360 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt @@ -1,4 +1,31 @@ @file:JvmName("Main") package com.github.serivesmejia.eocvsim -fun main() = EOCVSim().init() \ No newline at end of file +import picocli.CommandLine +import kotlin.system.exitProcess + +fun main(args: Array) { + val result = CommandLine( + EOCVSimCommandInterface() + ).execute(*args) + + exitProcess(result) +} + +@CommandLine.Command(name = "eocvsim", mixinStandardHelpOptions = true, version = [Build.versionString]) +class EOCVSimCommandInterface : Runnable { + + @CommandLine.Option(names = ["-w", "--workspace"]) + @JvmField var workspacePath = "" + + @CommandLine.Option(names = ["-p", "--pipeline"]) + @JvmField var initialPipeline = "" + + override fun run() { + println(workspacePath) + println(initialPipeline) + + EOCVSim().init() + } + +} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt index fff04b60..674ce8e5 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt @@ -27,7 +27,6 @@ import com.github.serivesmejia.eocvsim.gui.DialogFactory import com.github.serivesmejia.eocvsim.gui.dialog.Output import com.github.serivesmejia.eocvsim.pipeline.PipelineManager import com.github.serivesmejia.eocvsim.pipeline.PipelineSource -import com.github.serivesmejia.eocvsim.pipeline.compiler.util.PackageStatementRemover import com.github.serivesmejia.eocvsim.util.Log import com.github.serivesmejia.eocvsim.util.StrUtil import com.github.serivesmejia.eocvsim.util.SysUtil @@ -53,7 +52,6 @@ class CompiledPipelineManager(private val pipelineManager: PipelineManager) { val SOURCES_OUTPUT_FOLDER = File(COMPILER_FOLDER, File.separator + "gen_src").mkdirLazy() val CLASSES_OUTPUT_FOLDER = File(COMPILER_FOLDER, File.separator + "out_classes").mkdirLazy() val JARS_OUTPUT_FOLDER = File(COMPILER_FOLDER, File.separator + "out_jars").mkdirLazy() - val FLAT_SRC_FOLDER = File(COMPILER_FOLDER, File.separator + "flat_src").mkdirLazy() val PIPELINES_OUTPUT_JAR = File(JARS_OUTPUT_FOLDER, File.separator + "pipelines.jar") @@ -76,8 +74,6 @@ class CompiledPipelineManager(private val pipelineManager: PipelineManager) { val workspaceManager get() = pipelineManager.eocvSim.workspaceManager - private val visualizer get() = pipelineManager.eocvSim.visualizer - fun init() { Log.info(TAG, "Initializing...") asyncCompile(false) @@ -112,17 +108,12 @@ class CompiledPipelineManager(private val pipelineManager: PipelineManager) { workspaceManager.reloadConfig() val absoluteSourcesPath = workspaceManager.sourcesAbsolutePath.toFile() - Log.info(TAG, "Building java files in workspace, at ${absoluteSourcesPath.absolutePath}") val runtime = ElapsedTime() - val (files, sourcesPath) = if(workspaceManager.workspaceConfig.ignorePackages) { - PackageStatementRemover.processFiles(workspaceManager.sourceFiles) - } else Pair(workspaceManager.sourceFiles, absoluteSourcesPath) - val compiler = PipelineCompiler( - sourcesPath, files, + absoluteSourcesPath, workspaceManager.sourceFiles, workspaceManager.resourcesAbsolutePath.toFile(), workspaceManager.resourceFiles ) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/util/PackageStatementRemover.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/util/PackageStatementRemover.kt deleted file mode 100644 index 88c09754..00000000 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/util/PackageStatementRemover.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.pipeline.compiler.util - -import com.github.serivesmejia.eocvsim.pipeline.compiler.CompiledPipelineManager -import com.github.serivesmejia.eocvsim.util.SysUtil -import com.github.serivesmejia.eocvsim.util.extension.zeroBased -import java.io.File - -object PackageStatementRemover { - - fun processFiles(sourceFiles: List): Pair, File> { - val files = arrayListOf() - - SysUtil.deleteFilesUnder(CompiledPipelineManager.FLAT_SRC_FOLDER) - - for(file in sourceFiles) { - val destination = File(CompiledPipelineManager.FLAT_SRC_FOLDER.absolutePath + File.separator + file.name) - if(destination.exists()) { - throw IllegalStateException("Duplicate class file, name: ${file.name}. Make sure all the files in all the packages have a unique name") - } - - SysUtil.saveFileStr( - destination, - stripPackageDeclaration( - SysUtil.loadFileStr(file) - ) - ) - - files.add(destination) - } - - return Pair(files, CompiledPipelineManager.FLAT_SRC_FOLDER) - } - - val PACKAGE_DECLARATION_REGEX = Regex("package ([\\w&&\\D||\\n]([\\w\\.]*[\\w])?);") - - fun stripPackageDeclaration(src: String): String { - val match = PACKAGE_DECLARATION_REGEX.find(src) - - return if(match != null) { - val builder = StringBuilder() - val newLines = match.value.lines().size - 1 - - builder.append(src.subSequence(0, match.range.first)) - repeat(newLines) { - builder.appendLine() - } - builder.append(src.subSequence(match.range.last + 1, src.length.zeroBased)) - - builder.toString() - } else src - } - -} \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfig.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfig.java index c21e1959..061abb8f 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfig.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfig.java @@ -27,8 +27,6 @@ public class WorkspaceConfig { - public boolean ignorePackages = true; - public String sourcesPath = "."; public String resourcesPath = "."; From a24202087b74a3c7a321a3c15d92052a32bb6c25 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Mon, 30 Aug 2021 11:14:57 -0600 Subject: [PATCH 04/56] work on command line --- .../github/serivesmejia/eocvsim/EOCVSim.kt | 18 ++++++- .../com/github/serivesmejia/eocvsim/Main.kt | 47 ++++++++++++++++--- .../serivesmejia/eocvsim/gui/Visualizer.java | 22 ++++++--- .../eocvsim/pipeline/PipelineManager.kt | 17 +++++++ .../eocvsim/tuner/field/EnumField.kt | 4 +- .../eocvsim/workspace/WorkspaceManager.kt | 3 +- 6 files changed, 94 insertions(+), 17 deletions(-) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt index 287a0136..9c7f2255 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt @@ -31,6 +31,7 @@ import com.github.serivesmejia.eocvsim.gui.dialog.FileAlreadyExists import com.github.serivesmejia.eocvsim.input.InputSourceManager import com.github.serivesmejia.eocvsim.output.VideoRecordingSession import com.github.serivesmejia.eocvsim.pipeline.PipelineManager +import com.github.serivesmejia.eocvsim.pipeline.PipelineSource import com.github.serivesmejia.eocvsim.tuner.TunerManager import com.github.serivesmejia.eocvsim.util.FileFilters import com.github.serivesmejia.eocvsim.util.Log @@ -176,6 +177,16 @@ class EOCVSim(val params: Parameters = Parameters()) { } private fun start() { + if(params.initialPipelineName != null && params.initialPipelineSource != null) { + if(pipelineManager.compiledPipelineManager.isBuildRunning) { + pipelineManager.compiledPipelineManager.onBuildEnd { + pipelineManager.requestChangePipeline(params.initialPipelineName!!, params.initialPipelineSource!!) + } + } else { + pipelineManager.changePipeline(params.initialPipelineName!!, params.initialPipelineSource!!) + } + } + Log.info(TAG, "Begin EOCVSim loop") Log.blank() @@ -338,7 +349,7 @@ class EOCVSim(val params: Parameters = Parameters()) { private fun updateVisualizerTitle() { val isBuildRunning = if (pipelineManager.compiledPipelineManager.isBuildRunning) "(Building)" else "" - val workspaceMsg = " - ${config.workspacePath} $isBuildRunning" + val workspaceMsg = " - ${workspaceManager.workspaceFile.absolutePath} $isBuildRunning" val pipelineFpsMsg = " (${pipelineManager.pipelineFpsCounter.fps} Pipeline FPS)" val posterFpsMsg = " (${visualizer.viewport.matPoster.fpsCounter.fps} Poster FPS)" @@ -357,6 +368,11 @@ class EOCVSim(val params: Parameters = Parameters()) { class Parameters { var scanForPipelinesIn = "org.firstinspires" var scanForTunableFieldsIn = "com.github.serivesmejia" + + var initialWorkspace: File? = null + + var initialPipelineName: String? = null + var initialPipelineSource: PipelineSource? = null } } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt index 2dd20360..b1bc7f8c 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt @@ -1,13 +1,18 @@ @file:JvmName("Main") package com.github.serivesmejia.eocvsim +import com.github.serivesmejia.eocvsim.pipeline.PipelineSource +import com.github.serivesmejia.eocvsim.util.Log import picocli.CommandLine +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths import kotlin.system.exitProcess fun main(args: Array) { val result = CommandLine( EOCVSimCommandInterface() - ).execute(*args) + ).setCaseInsensitiveEnumValuesAllowed(true).execute(*args) exitProcess(result) } @@ -15,17 +20,47 @@ fun main(args: Array) { @CommandLine.Command(name = "eocvsim", mixinStandardHelpOptions = true, version = [Build.versionString]) class EOCVSimCommandInterface : Runnable { - @CommandLine.Option(names = ["-w", "--workspace"]) + @CommandLine.Option(names = ["-w", "--workspace"], description = ["Specifies the workspace that will be used only during this run, path can be relative and absolute"]) @JvmField var workspacePath = "" - @CommandLine.Option(names = ["-p", "--pipeline"]) + @CommandLine.Option(names = ["-p", "--pipeline"], description = ["Specifies the pipeline selected when the simulator starts, and the initial runtime build finishes if it was running"]) @JvmField var initialPipeline = "" + @CommandLine.Option(names = ["-s", "--source"], description = ["Specifies the source of the pipeline that will be selected when the simulator starts, from the --pipeline argument. Defaults to CLASSPATH. Possible values: \${COMPLETION-CANDIDATES}"]) + @JvmField var source = PipelineSource.CLASSPATH + override fun run() { - println(workspacePath) - println(initialPipeline) + val parameters = EOCVSim.Parameters() + + if(workspacePath.trim() != "") { + var file = File(workspacePath) + + if(!file.exists()) { + file = Paths.get(System.getProperty("user.dir"), workspacePath).toFile() + + if(!file.exists()) { + Log.error("Workspace path is not valid, folder doesn't exist (tried in \"$workspacePath\" and \"${file.absolutePath})\"") + + exitProcess(1) + } + } + + if(!file.isDirectory) { + Log.error("Workspace path is not valid, the specified path is not a folder") + exitProcess(1) + } + + Log.info("Workspace from command line: ${file.absolutePath}") + + parameters.initialWorkspace = file + } + + if(initialPipeline.trim() != "") { + parameters.initialPipelineName = initialPipeline + parameters.initialPipelineSource = PipelineSource.CLASSPATH + } - EOCVSim().init() + EOCVSim(parameters).init() } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java index 732b6f61..c75fc73d 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java @@ -215,6 +215,10 @@ public void init(Theme theme) { registerListeners(); hasFinishedInitializing = true; + + if(!PipelineCompiler.Companion.getIS_USABLE()) { + compilingUnsupported(); + } } public void initAsync(Theme simTheme) { @@ -376,16 +380,20 @@ public void asyncCompilePipelines() { return Unit.INSTANCE; }); } else { - asyncPleaseWaitDialog( - "Runtime compilation is not supported on this JVM", - "For further info, check the EOCV-Sim GitHub repo", - "Close", - new Dimension(320, 160), - true, true - ); + compilingUnsupported(); } } + public void compilingUnsupported() { + asyncPleaseWaitDialog( + "Runtime compiling is not supported on this JVM", + "For further info, check the EOCV-Sim GitHub repo", + "Close", + new Dimension(320, 160), + true, true + ); + } + public void selectPipelinesWorkspace() { DialogFactory.createFileChooser( frame, DialogFactory.FileChooser.Mode.DIRECTORY_SELECT diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt index 5ec974ce..ac947670 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt @@ -371,6 +371,23 @@ class PipelineManager(var eocvSim: EOCVSim) { } } + fun changePipeline(name: String, source: PipelineSource) { + for((i, data) in pipelines.withIndex()) { + if(data.clazz.simpleName.equals(name, true) && data.source == source) { + changePipeline(i) + return + } + } + + Log.warn(TAG, "Pipeline class with name $name and source $source couldn't be found") + } + + fun requestChangePipeline(name: String, source: PipelineSource) { + eocvSim.onMainUpdate.doOnce { + changePipeline(name, source) + } + } + /** * Changes to the requested pipeline, no matter * if we're currently on the same pipeline or not diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/EnumField.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/EnumField.kt index 30aa7405..073ee51a 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/EnumField.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/EnumField.kt @@ -56,9 +56,9 @@ class EnumField(private val instance: OpenCvPipeline, return values } - override fun hasChanged() = reflectionField.get(instance) == beforeValue + override fun hasChanged() = reflectionField.get(instance) != beforeValue - class EnumFieldAcceptor: TunableFieldAcceptor { + class EnumFieldAcceptor : TunableFieldAcceptor { override fun accept(clazz: Class<*>) = clazz.isEnum } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/WorkspaceManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/WorkspaceManager.kt index 386c2a5b..dd6ec5c2 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/WorkspaceManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/WorkspaceManager.kt @@ -169,7 +169,8 @@ class WorkspaceManager(val eocvSim: EOCVSim) { } } - val file = File(eocvSim.config.workspacePath) + val file = eocvSim.params.initialWorkspace ?: File(eocvSim.config.workspacePath) + workspaceFile = if(file.exists()) file else From 60581340b9d76fe1a17c11fa816e0d11f2021971 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Tue, 31 Aug 2021 17:09:20 -0600 Subject: [PATCH 05/56] Current command line parameters working & optimized icon loading --- .../github/serivesmejia/eocvsim/EOCVSim.kt | 12 +--------- .../com/github/serivesmejia/eocvsim/Main.kt | 6 +++-- .../github/serivesmejia/eocvsim/gui/Icons.kt | 4 ++++ .../tuner/TunableFieldPanelOptions.kt | 8 +++---- .../gui/util/icon/PipelineListIconRenderer.kt | 8 +++---- .../util/icon/SourcesListIconRenderer.java | 23 +++++++++++-------- .../eocvsim/pipeline/PipelineManager.kt | 19 ++++++++++++--- 7 files changed, 47 insertions(+), 33 deletions(-) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt index 9c7f2255..717bec85 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt @@ -177,16 +177,6 @@ class EOCVSim(val params: Parameters = Parameters()) { } private fun start() { - if(params.initialPipelineName != null && params.initialPipelineSource != null) { - if(pipelineManager.compiledPipelineManager.isBuildRunning) { - pipelineManager.compiledPipelineManager.onBuildEnd { - pipelineManager.requestChangePipeline(params.initialPipelineName!!, params.initialPipelineSource!!) - } - } else { - pipelineManager.changePipeline(params.initialPipelineName!!, params.initialPipelineSource!!) - } - } - Log.info(TAG, "Begin EOCVSim loop") Log.blank() @@ -273,7 +263,7 @@ class EOCVSim(val params: Parameters = Parameters()) { Log.blank() Thread( - { EOCVSim().init() }, + { EOCVSim(params).init() }, "main" ).start() //run next instance on a separate thread for the old one to get interrupted and ended } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt index b1bc7f8c..998c920f 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt @@ -27,7 +27,7 @@ class EOCVSimCommandInterface : Runnable { @JvmField var initialPipeline = "" @CommandLine.Option(names = ["-s", "--source"], description = ["Specifies the source of the pipeline that will be selected when the simulator starts, from the --pipeline argument. Defaults to CLASSPATH. Possible values: \${COMPLETION-CANDIDATES}"]) - @JvmField var source = PipelineSource.CLASSPATH + @JvmField var initialPipelineSource = PipelineSource.CLASSPATH override fun run() { val parameters = EOCVSim.Parameters() @@ -57,7 +57,9 @@ class EOCVSimCommandInterface : Runnable { if(initialPipeline.trim() != "") { parameters.initialPipelineName = initialPipeline - parameters.initialPipelineSource = PipelineSource.CLASSPATH + parameters.initialPipelineSource = initialPipelineSource + + Log.info("Initial pipeline from command line: $initialPipeline coming from $initialPipelineSource") } EOCVSim(parameters).init() diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt index 6f30dbd9..7a981bf2 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt @@ -76,6 +76,10 @@ object Icons { return icons[name]!! } + fun lazyGetImageResized(name: String, width: Int, height: Int) = lazy { + getImageResized(name, width, height) + } + fun getImageResized(name: String, width: Int, height: Int): ImageIcon { //determines the icon name from the: //name, widthxheight, is inverted or is original diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelOptions.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelOptions.kt index 18679ea8..6dc1e481 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelOptions.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelOptions.kt @@ -39,10 +39,10 @@ import javax.swing.event.AncestorListener class TunableFieldPanelOptions(val fieldPanel: TunableFieldPanel, eocvSim: EOCVSim) : JPanel() { - private val sliderIco = Icons.getImageResized("ico_slider", 15, 15) - private val textBoxIco = Icons.getImageResized("ico_textbox", 15, 15) - private val configIco = Icons.getImageResized("ico_config", 15, 15) - private val colorPickIco = Icons.getImageResized("ico_colorpick", 15, 15) + private val sliderIco by Icons.lazyGetImageResized("ico_slider", 15, 15) + private val textBoxIco by Icons.lazyGetImageResized("ico_textbox", 15, 15) + private val configIco by Icons.lazyGetImageResized("ico_config", 15, 15) + private val colorPickIco by Icons.lazyGetImageResized("ico_colorpick", 15, 15) private val textBoxSliderToggle = JToggleButton() private val configButton = JButton() diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/PipelineListIconRenderer.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/PipelineListIconRenderer.kt index 1c1a2e82..dd3d949f 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/PipelineListIconRenderer.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/PipelineListIconRenderer.kt @@ -12,8 +12,8 @@ class PipelineListIconRenderer( private val pipelineManager: PipelineManager ) : DefaultListCellRenderer() { - private val gearsIcon = Icons.getImageResized("ico_gears", 15, 15) - private val hammerIcon = Icons.getImageResized("ico_hammer", 15, 15) + private val gearsIcon by Icons.lazyGetImageResized("ico_gears", 15, 15) + private val hammerIcon by Icons.lazyGetImageResized("ico_hammer", 15, 15) override fun getListCellRendererComponent( list: JList<*>, @@ -33,10 +33,10 @@ class PipelineListIconRenderer( if(runtimePipelinesAmount > 0) { val source = pipelineManager.pipelines[index].source - label.setIcon(when(source) { + label.icon = when(source) { PipelineSource.COMPILED_ON_RUNTIME -> gearsIcon else -> hammerIcon - }) + } } return label diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/SourcesListIconRenderer.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/SourcesListIconRenderer.java index 24c3e243..0795e245 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/SourcesListIconRenderer.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/SourcesListIconRenderer.java @@ -31,20 +31,16 @@ public class SourcesListIconRenderer extends DefaultListCellRenderer { - private ImageIcon imgIcon = null; - private ImageIcon camIcon = null; - private ImageIcon vidIcon = null; - public static final int ICO_W = 15; public static final int ICO_H = 15; public InputSourceManager sourceManager = null; - public SourcesListIconRenderer(InputSourceManager sourceManager) { - imgIcon = Icons.INSTANCE.getImageResized("ico_img", 15, 15); - camIcon = Icons.INSTANCE.getImageResized("ico_cam", 15, 15); - vidIcon = Icons.INSTANCE.getImageResized("ico_vid", 15, 15); + ImageIcon imageIcon = null; + ImageIcon camIcon = null; + ImageIcon vidIcon = null; + public SourcesListIconRenderer(InputSourceManager sourceManager) { this.sourceManager = sourceManager; } @@ -62,12 +58,21 @@ public Component getListCellRendererComponent( switch (sourceManager.getSourceType((String) value)) { case IMAGE: - label.setIcon(imgIcon); + if(imageIcon == null) { + imageIcon = Icons.INSTANCE.getImageResized("ico_img", 15, 15); + } + label.setIcon(imageIcon); break; case CAMERA: + if(camIcon == null) { + camIcon = Icons.INSTANCE.getImageResized("ico_cam", 15, 15); + } label.setIcon(camIcon); break; case VIDEO: + if(vidIcon == null) { + vidIcon = Icons.INSTANCE.getImageResized("ico_vid", 15, 15); + } label.setIcon(vidIcon); break; } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt index ac947670..c3690512 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt @@ -38,7 +38,6 @@ import org.firstinspires.ftc.robotcore.external.Telemetry import org.opencv.core.Mat import org.openftc.easyopencv.OpenCvPipeline import org.openftc.easyopencv.TimestampedPipelineHandler -import java.awt.Dimension import java.lang.reflect.Constructor import java.util.* import kotlin.coroutines.EmptyCoroutineContext @@ -133,6 +132,7 @@ class PipelineManager(var eocvSim: EOCVSim) { compiledPipelineManager.init() + // changing to initial pipeline onUpdate.doOnce { if(compiledPipelineManager.isBuildRunning) compiledPipelineManager.onBuildEnd.doOnce(::applyStaticSnapOrDef) @@ -162,8 +162,16 @@ class PipelineManager(var eocvSim: EOCVSim) { private fun applyStaticSnapOrDef() { onUpdate.doOnce { - if(!applyStaticSnapshot()) - forceChangePipeline(0) + if(!applyStaticSnapshot()) { + val params = eocvSim.params + + // changing to the initial pipeline, defined by the eocv sim parameters or the default pipeline + if(params.initialPipelineName != null) { + changePipeline(params.initialPipelineName!!, params.initialPipelineSource ?: PipelineSource.CLASSPATH) + } else { + forceChangePipeline(0) + } + } eocvSim.visualizer.pipelineSelectorPanel.allowPipelineSwitching = true } @@ -377,6 +385,11 @@ class PipelineManager(var eocvSim: EOCVSim) { changePipeline(i) return } + + if(data.clazz.name.equals(name, true) && data.source == source) { + changePipeline(i) + return + } } Log.warn(TAG, "Pipeline class with name $name and source $source couldn't be found") From 8f4f9d1fc3b1813d2af7896249907f12951fb467 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Wed, 1 Sep 2021 10:12:48 -0600 Subject: [PATCH 06/56] Fix source selector stuck in infinite loop --- .../github/serivesmejia/eocvsim/EOCVSim.kt | 25 +++++++-- .../com/github/serivesmejia/eocvsim/Main.kt | 7 +-- .../visualizer/SourceSelectorPanel.kt | 52 +++++++++---------- .../gui/dialog/source/CreateCameraSource.java | 3 +- .../gui/dialog/source/CreateImageSource.java | 11 ++-- .../gui/dialog/source/CreateVideoSource.java | 5 +- .../eocvsim/input/InputSourceManager.java | 47 +++++++++++++---- .../eocvsim/pipeline/PipelineManager.kt | 38 ++++++++------ .../eocvsim/util/event/EventHandler.kt | 14 ++++- .../EOCVSimUncaughtExceptionHandler.kt | 3 +- .../eocvsim/util/io/FileWatcher.kt | 2 +- 11 files changed, 134 insertions(+), 73 deletions(-) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt index 717bec85..bcf7bcc3 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt @@ -167,6 +167,8 @@ class EOCVSim(val params: Parameters = Parameters()) { visualizer.sourceSelectorPanel.updateSourcesList() //update sources and pick first one visualizer.sourceSelectorPanel.sourceSelector.selectedIndex = 0 + visualizer.sourceSelectorPanel.allowSourceSwitching = true + visualizer.pipelineSelectorPanel.updatePipelinesList() //update pipelines and pick first one (DefaultPipeline) visualizer.pipelineSelectorPanel.selectedIndex = 0 @@ -190,7 +192,11 @@ class EOCVSim(val params: Parameters = Parameters()) { tunerManager.update() try { - pipelineManager.update(inputSourceManager.lastMatFromSource) + pipelineManager.update( + if(inputSourceManager.lastMatFromSource != null && !inputSourceManager.lastMatFromSource.empty()) { + inputSourceManager.lastMatFromSource + } else null + ) } catch (ex: MaxActiveContextsException) { //handles when a lot of pipelines are stuck in the background visualizer.asyncPleaseWaitDialog( "There are many pipelines stuck in processFrame running in the background", @@ -233,7 +239,7 @@ class EOCVSim(val params: Parameters = Parameters()) { } fun destroy(reason: DestroyReason) { - Log.warn(TAG, "Destroying current EOCVSim ($hexCode) due to $reason, it is normal to see InterruptedExceptions and other kinds of stack traces blow") + Log.warn(TAG, "Destroying current EOCVSim ($hexCode) due to $reason, it is normal to see InterruptedExceptions and other kinds of stack traces below") //stop recording session if there's currently an ongoing one currentRecordingSession?.stopRecordingSession() @@ -247,6 +253,9 @@ class EOCVSim(val params: Parameters = Parameters()) { visualizer.close() eocvSimThread.interrupt() + + if(reason == DestroyReason.USER_REQUESTED || reason == DestroyReason.CRASH) + jvmMainThread.interrupt() } fun destroy() { @@ -262,10 +271,16 @@ class EOCVSim(val params: Parameters = Parameters()) { destroy(DestroyReason.RESTART) Log.blank() - Thread( + currentMainThread = Thread( { EOCVSim(params).init() }, - "main" - ).start() //run next instance on a separate thread for the old one to get interrupted and ended + "new-main" + ) + currentMainThread.start() //run next instance on a new main thread for the old one to get interrupted and ended + + if(Thread.currentThread() == jvmMainThread) { + Thread.interrupted() // clear interrupt state + Thread.sleep(Long.MAX_VALUE) // hang forever the jvm main thread so that the app doesnt die idk + } } fun startRecordingSession() { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt index 998c920f..1330354e 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt @@ -5,10 +5,12 @@ import com.github.serivesmejia.eocvsim.pipeline.PipelineSource import com.github.serivesmejia.eocvsim.util.Log import picocli.CommandLine import java.io.File -import java.nio.file.Path import java.nio.file.Paths import kotlin.system.exitProcess +val jvmMainThread: Thread = Thread.currentThread() +var currentMainThread: Thread = jvmMainThread + fun main(args: Array) { val result = CommandLine( EOCVSimCommandInterface() @@ -20,7 +22,7 @@ fun main(args: Array) { @CommandLine.Command(name = "eocvsim", mixinStandardHelpOptions = true, version = [Build.versionString]) class EOCVSimCommandInterface : Runnable { - @CommandLine.Option(names = ["-w", "--workspace"], description = ["Specifies the workspace that will be used only during this run, path can be relative and absolute"]) + @CommandLine.Option(names = ["-w", "--workspace"], description = ["Specifies the workspace that will be used only during this run, path can be relative or absolute"]) @JvmField var workspacePath = "" @CommandLine.Option(names = ["-p", "--pipeline"], description = ["Specifies the pipeline selected when the simulator starts, and the initial runtime build finishes if it was running"]) @@ -40,7 +42,6 @@ class EOCVSimCommandInterface : Runnable { if(!file.exists()) { Log.error("Workspace path is not valid, folder doesn't exist (tried in \"$workspacePath\" and \"${file.absolutePath})\"") - exitProcess(1) } } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/SourceSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/SourceSelectorPanel.kt index a46bd996..a0167cb8 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/SourceSelectorPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/SourceSelectorPanel.kt @@ -29,7 +29,7 @@ class SourceSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { private var lastCreateSourcePopup: PopupX? = null - var allowSourceSwitching = true + var allowSourceSwitching = false init { layout = GridBagLayout() @@ -92,38 +92,38 @@ class SourceSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { private fun registerListeners() { //listener for changing input sources sourceSelector.addListSelectionListener { evt -> - if(!allowSourceSwitching) return@addListSelectionListener - - try { - if (sourceSelector.selectedIndex != -1) { - val model = sourceSelector.model - val source = model.getElementAt(sourceSelector.selectedIndex) - - //enable or disable source delete button depending if source is default or not - eocvSim.visualizer.sourceSelectorPanel.sourceSelectorDeleteBtt - .isEnabled = !(eocvSim.inputSourceManager.sources[source]?.isDefault ?: true) - - if (!evt.valueIsAdjusting && source != beforeSelectedSource) { - if (!eocvSim.pipelineManager.paused) { - eocvSim.inputSourceManager.requestSetInputSource(source) - beforeSelectedSource = source - beforeSelectedSourceIndex = sourceSelector.selectedIndex - } else { - //check if the user requested the pause or if it was due to one shoot analysis when selecting images - if (eocvSim.pipelineManager.pauseReason !== PipelineManager.PauseReason.IMAGE_ONE_ANALYSIS) { - sourceSelector.setSelectedIndex(beforeSelectedSourceIndex) - } else { //handling pausing - eocvSim.pipelineManager.requestSetPaused(false) + if(allowSourceSwitching && !evt.valueIsAdjusting) { + try { + if (sourceSelector.selectedIndex != -1) { + val model = sourceSelector.model + val source = model.getElementAt(sourceSelector.selectedIndex) + + //enable or disable source delete button depending if source is default or not + eocvSim.visualizer.sourceSelectorPanel.sourceSelectorDeleteBtt + .isEnabled = !(eocvSim.inputSourceManager.sources[source]?.isDefault ?: true) + + if (!evt.valueIsAdjusting && source != beforeSelectedSource) { + if (!eocvSim.pipelineManager.paused) { eocvSim.inputSourceManager.requestSetInputSource(source) beforeSelectedSource = source beforeSelectedSourceIndex = sourceSelector.selectedIndex + } else { + //check if the user requested the pause or if it was due to one shoot analysis when selecting images + if (eocvSim.pipelineManager.pauseReason !== PipelineManager.PauseReason.IMAGE_ONE_ANALYSIS) { + sourceSelector.setSelectedIndex(beforeSelectedSourceIndex) + } else { //handling pausing + eocvSim.pipelineManager.requestSetPaused(false) + eocvSim.inputSourceManager.requestSetInputSource(source) + beforeSelectedSource = source + beforeSelectedSourceIndex = sourceSelector.selectedIndex + } } } + } else { + sourceSelector.setSelectedIndex(1) } - } else { - sourceSelector.setSelectedIndex(1) + } catch (ignored: ArrayIndexOutOfBoundsException) { } - } catch (ignored: ArrayIndexOutOfBoundsException) { } } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateCameraSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateCameraSource.java index e946b79d..ffc958dd 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateCameraSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateCameraSource.java @@ -267,7 +267,8 @@ private void updateState() { public void createSource(String sourceName, int index, Size size) { eocvSim.onMainUpdate.doOnce(() -> eocvSim.inputSourceManager.addInputSource( sourceName, - new CameraSource(index, size) + new CameraSource(index, size), + true )); } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateImageSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateImageSource.java index 3c5cf2a9..a7a9e9b5 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateImageSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateImageSource.java @@ -192,10 +192,13 @@ public void close() { } public void createSource(String sourceName, String imgPath, Size size) { - eocvSim.onMainUpdate.doOnce(() -> eocvSim.inputSourceManager.addInputSource( - sourceName, - new ImageSource(imgPath, size) - )); + eocvSim.onMainUpdate.doOnce(() -> + eocvSim.inputSourceManager.addInputSource( + sourceName, + new ImageSource(imgPath, size), + false + ) + ); } public void updateCreateBtt() { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateVideoSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateVideoSource.java index ec94b739..5b3a0ac6 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateVideoSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateVideoSource.java @@ -205,10 +205,9 @@ public void createSource(String sourceName, String videoPath, Size size) { eocvSim.onMainUpdate.doOnce(() -> { eocvSim.inputSourceManager.addInputSource( sourceName, - new VideoSource(videoPath, size) + new VideoSource(videoPath, size), + true ); - - eocvSim.inputSourceManager.requestSetInputSource(sourceName); }); } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java index f677e2ec..5ba73955 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java @@ -25,6 +25,7 @@ import com.github.serivesmejia.eocvsim.EOCVSim; import com.github.serivesmejia.eocvsim.gui.Visualizer; +import com.github.serivesmejia.eocvsim.gui.component.visualizer.SourceSelectorPanel; import com.github.serivesmejia.eocvsim.input.source.ImageSource; import com.github.serivesmejia.eocvsim.pipeline.PipelineManager; import com.github.serivesmejia.eocvsim.util.Log; @@ -32,7 +33,7 @@ import org.opencv.core.Mat; import org.opencv.core.Size; -import javax.swing.SwingUtilities; +import javax.swing.*; import java.awt.*; import java.io.File; import java.io.IOException; @@ -49,10 +50,13 @@ public class InputSourceManager { public volatile HashMap sources = new HashMap<>(); public InputSourceLoader inputSourceLoader = new InputSourceLoader(); + public SourceSelectorPanel selectorPanel; public InputSourceManager(EOCVSim eocvSim) { this.eocvSim = eocvSim; + selectorPanel = eocvSim.visualizer.sourceSelectorPanel; } + public void init() { Log.info("InputSourceManager", "Initializing..."); @@ -65,6 +69,8 @@ public void init() { createDefaultImgInputSource("/images/ug_1.jpg", "ug_eocvsim_1.jpg", "Ultimate Goal 1 Ring", size); createDefaultImgInputSource("/images/ug_0.jpg", "ug_eocvsim_0.jpg", "Ultimate Goal 0 Ring", size); + setInputSource("Ultimate Goal 4 Ring"); + inputSourceLoader.loadInputSourcesFromFile(); for (Map.Entry entry : inputSourceLoader.loadedInputSources.entrySet()) { @@ -103,13 +109,21 @@ public void update(boolean isPaused) { } } + public void addInputSource(String name, InputSource inputSource) { + addInputSource(name, inputSource, false); + } + + public void addInputSource(String name, InputSource inputSource, boolean dispatchedByUser) { if (inputSource == null) { return; } if (sources.containsKey(name)) return; + if(eocvSim.visualizer.sourceSelectorPanel != null) { + eocvSim.visualizer.sourceSelectorPanel.setAllowSourceSwitching(false); + } inputSource.name = name; sources.put(name, inputSource); @@ -123,18 +137,31 @@ public void addInputSource(String name, InputSource inputSource) { } if(eocvSim.visualizer.sourceSelectorPanel != null) { - eocvSim.visualizer.sourceSelectorPanel.updateSourcesList(); + SourceSelectorPanel selectorPanel = eocvSim.visualizer.sourceSelectorPanel; + + selectorPanel.updateSourcesList(); SwingUtilities.invokeLater(() -> { - int index = eocvSim.visualizer.sourceSelectorPanel.getIndexOf(name); + JList sourceSelector = selectorPanel.getSourceSelector(); + + int currentSourceIndex = sourceSelector.getSelectedIndex(); + + if(dispatchedByUser) { + int index = selectorPanel.getIndexOf(name); - eocvSim.visualizer.sourceSelectorPanel - .getSourceSelector().setSelectedIndex(index); + sourceSelector.setSelectedIndex(index); + + requestSetInputSource(name); + + eocvSim.onMainUpdate.doOnce(() -> { + eocvSim.pipelineManager.requestSetPaused(false); + pauseIfImageTwoFrames(); + }); + } else { + sourceSelector.setSelectedIndex(currentSourceIndex); + } - eocvSim.onMainUpdate.doOnce(() -> { - eocvSim.pipelineManager.requestSetPaused(false); - pauseIfImageTwoFrames(); - }); + selectorPanel.setAllowSourceSwitching(true); }); } @@ -236,7 +263,7 @@ public Visualizer.AsyncPleaseWaitDialog showApwdIfNeeded(String sourceName) { new Dimension(300, 150), true ); - apwd.onCancel(() -> eocvSim.destroy()); + apwd.onCancel(eocvSim::destroy); } return apwd; diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt index c3690512..28b8d4e3 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt @@ -177,7 +177,7 @@ class PipelineManager(var eocvSim: EOCVSim) { } } - fun update(inputMat: Mat) { + fun update(inputMat: Mat?) { onUpdate.run() if(activePipelineContexts.size > MAX_ALLOWED_ACTIVE_PIPELINE_CONTEXTS) { @@ -217,7 +217,7 @@ class PipelineManager(var eocvSim: EOCVSim) { //a different pipeline at this point. we also call init if we //haven't done so. - if(!hasInitCurrentPipeline) { + if(!hasInitCurrentPipeline && inputMat != null) { currentPipeline?.init(inputMat) Log.info("PipelineManager", "Initialized pipeline $currentPipelineName") @@ -228,26 +228,30 @@ class PipelineManager(var eocvSim: EOCVSim) { //check if we're still active (not timeouted) //after initialization - currentPipeline?.processFrame(inputMat)?.let { outputMat -> - if (isActive) { - pipelineFpsCounter.update() - - for (poster in pipelineOutputPosters.toTypedArray()) { - try { - poster.post(outputMat) - } catch (ex: Exception) { - Log.error( - TAG, - "Uncaught exception thrown while posting pipeline output Mat to ${poster.name} poster", - ex - ) + if(inputMat != null) { + currentPipeline?.processFrame(inputMat)?.let { outputMat -> + if (isActive) { + pipelineFpsCounter.update() + + for (poster in pipelineOutputPosters.toTypedArray()) { + try { + poster.post(outputMat) + } catch (ex: Exception) { + Log.error( + TAG, + "Uncaught exception thrown while posting pipeline output Mat to ${poster.name} poster", + ex + ) + } } } - } else { - activePipelineContexts.remove(this.coroutineContext) } } + if(!isActive) { + activePipelineContexts.remove(this.coroutineContext) + } + updateExceptionTracker() } catch (ex: Exception) { //handling exceptions from pipelines updateExceptionTracker(ex) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt index 99b68bb8..97bf5d58 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt @@ -54,7 +54,12 @@ class EventHandler(val name: String) : Runnable { try { runListener(listener, false) } catch (ex: Exception) { - Log.warn("${name}-EventHandler", "Error while running listener ${listener.javaClass.name}", ex) + if(ex is InterruptedException) { + Log.warn("${name}-EventHandler", "Rethrowing InterruptedException...") + throw ex + } else { + Log.warn("${name}-EventHandler", "Error while running listener ${listener.javaClass.name}", ex) + } } } @@ -65,7 +70,12 @@ class EventHandler(val name: String) : Runnable { try { runListener(listener, true) } catch (ex: Exception) { - Log.warn("${name}-EventHandler", "Error while running \"once\" ${listener.javaClass.name}", ex) + if(ex is InterruptedException) { + Log.warn("${name}-EventHandler", "Rethrowing InterruptedException...") + throw ex + } else { + Log.warn("${name}-EventHandler", "Error while running \"once\" ${listener.javaClass.name}", ex) + } } toRemoveOnceListeners.add(listener) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/EOCVSimUncaughtExceptionHandler.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/EOCVSimUncaughtExceptionHandler.kt index 54112afa..0ed21ce2 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/EOCVSimUncaughtExceptionHandler.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/EOCVSimUncaughtExceptionHandler.kt @@ -1,5 +1,6 @@ package com.github.serivesmejia.eocvsim.util.exception.handling +import com.github.serivesmejia.eocvsim.currentMainThread import com.github.serivesmejia.eocvsim.util.Log import kotlin.system.exitProcess @@ -33,7 +34,7 @@ class EOCVSimUncaughtExceptionHandler private constructor() : Thread.UncaughtExc //Exit if uncaught exception happened in the main thread //since we would be basically in a deadlock state if that happened //or if we have a lotta uncaught exceptions. - if(t.name.equals("main", true) || uncaughtExceptionsCount > MAX_UNCAUGHT_EXCEPTIONS_BEFORE_CRASH) { + if(t == currentMainThread || uncaughtExceptionsCount > MAX_UNCAUGHT_EXCEPTIONS_BEFORE_CRASH) { CrashReport(e).saveCrashReport() Log.warn(TAG, "If this error persists, open an issue on EOCV-Sim's GitHub attaching the crash report file.") diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/io/FileWatcher.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/io/FileWatcher.kt index c5bdba5c..ae003668 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/io/FileWatcher.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/io/FileWatcher.kt @@ -65,7 +65,7 @@ class FileWatcher(private val watchingDirectories: List, } } - Thread.sleep(800) //check every 800 ms + Thread.sleep(1200) //check every 800 ms } Log.info(TAG, "Stopping watching directories:\n$directoriesList") From d95753834ff334375509ab04e091ebfa5a3d319b Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Wed, 1 Sep 2021 23:10:09 -0600 Subject: [PATCH 07/56] add error log file creation --- .../main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt | 3 ++- .../eocvsim/util/exception/handling/CrashReport.kt | 7 +++++++ .../exception/handling/EOCVSimUncaughtExceptionHandler.kt | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt index bcf7bcc3..56e1fec6 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt @@ -50,6 +50,7 @@ import java.io.File import javax.swing.SwingUtilities import javax.swing.filechooser.FileFilter import javax.swing.filechooser.FileNameExtensionFilter +import kotlin.concurrent.thread import kotlin.system.exitProcess class EOCVSim(val params: Parameters = Parameters()) { @@ -230,7 +231,7 @@ class EOCVSim(val params: Parameters = Parameters()) { //updating displayed telemetry visualizer.telemetryPanel.updateTelemetry(pipelineManager.currentTelemetry) - //limit FPS + //limit FPG fpsLimiter.maxFPS = config.pipelineMaxFps.fps.toDouble() fpsLimiter.sync() } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/CrashReport.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/CrashReport.kt index fcfe00a4..dbd38490 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/CrashReport.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/CrashReport.kt @@ -106,6 +106,13 @@ class CrashReport(causedByException: Throwable) { saveCrashReport(crashLogFile) } + fun saveCrashReport(filename: String) { + val workingDir = File(System.getProperty("user.dir")) + val crashLogFile = workingDir + "/$filename.log" + + saveCrashReport(crashLogFile) + } + override fun toString() = sb.toString() } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/EOCVSimUncaughtExceptionHandler.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/EOCVSimUncaughtExceptionHandler.kt index 0ed21ce2..4451cd8a 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/EOCVSimUncaughtExceptionHandler.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/EOCVSimUncaughtExceptionHandler.kt @@ -43,6 +43,8 @@ class EOCVSimUncaughtExceptionHandler private constructor() : Thread.UncaughtExc exitProcess(1) } else { + CrashReport(e).saveCrashReport("lasterror-eocvsim") + //if not, eocv sim might still be working (i.e a crash from a MatPoster thread) //so we might not need to exit in this point, but we'll need to send a warning //to the user From e641a8f4f0f4861fbd8d6d5a2244ae6dcd07211b Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Sun, 5 Sep 2021 12:59:51 -0600 Subject: [PATCH 08/56] Add NodeEye module --- .../eocvsim/pipeline/PipelineManager.kt | 10 +++- .../compiler/CompiledPipelineManager.kt | 4 +- .../pipeline/compiler/PipelineClassLoader.kt | 2 + NodeEye/build.gradle | 9 +++ .../io/github/deltacv/nodeeye/NodeEye.kt | 55 +++++++++++++++++++ imgui.ini | 5 ++ settings.gradle | 4 +- 7 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 NodeEye/build.gradle create mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/NodeEye.kt create mode 100644 imgui.ini diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt index 28b8d4e3..65760fce 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt @@ -72,6 +72,8 @@ class PipelineManager(var eocvSim: EOCVSim) { @Volatile var currentPipeline: OpenCvPipeline? = null private set + @Volatile var currentPipelineData: PipelineData? = null + private set var currentPipelineName = "" private set var currentPipelineIndex = -1 @@ -459,6 +461,7 @@ class PipelineManager(var eocvSim: EOCVSim) { } currentPipeline = nextPipeline + currentPipelineData = pipelines[index] currentTelemetry = nextTelemetry currentPipelineIndex = index currentPipelineName = currentPipeline!!.javaClass.simpleName @@ -544,11 +547,12 @@ class PipelineManager(var eocvSim: EOCVSim) { return false } - fun getIndexOf(pipeline: OpenCvPipeline) = getIndexOf(pipeline::class.java) + fun getIndexOf(pipeline: OpenCvPipeline, source: PipelineSource = PipelineSource.CLASSPATH) = + getIndexOf(pipeline::class.java, source) - fun getIndexOf(pipelineClass: Class): Int? { + fun getIndexOf(pipelineClass: Class, source: PipelineSource = PipelineSource.CLASSPATH): Int? { for((i, pipelineData) in pipelines.withIndex()) { - if(pipelineData.clazz.name == pipelineClass.name) { + if(pipelineData.clazz.name == pipelineClass.name && pipelineData.source == source) { return i } } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt index 674ce8e5..96420ea1 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt @@ -156,14 +156,14 @@ class CompiledPipelineManager(private val pipelineManager: PipelineManager) { } } - val beforePipeline = pipelineManager.currentPipeline + val beforePipeline = pipelineManager.currentPipelineData pipelineManager.onUpdate.doOnce { pipelineManager.refreshGuiPipelineList() if(fixSelectedPipeline) { if(beforePipeline != null) { - val pipeline = pipelineManager.getIndexOf(beforePipeline) + val pipeline = pipelineManager.getIndexOf(beforePipeline.clazz, beforePipeline.source) pipelineManager.forceChangePipeline(pipeline, true) } else { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineClassLoader.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineClassLoader.kt index 40ff6fa2..342645a6 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineClassLoader.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineClassLoader.kt @@ -84,6 +84,8 @@ class PipelineClassLoader(pipelinesJar: File) : ClassLoader() { } override fun getResourceAsStream(name: String): InputStream? { + println("trying to load $name") + val entry = zipFile.getEntry(name) if(entry != null) { diff --git a/NodeEye/build.gradle b/NodeEye/build.gradle new file mode 100644 index 00000000..2ea0c0d3 --- /dev/null +++ b/NodeEye/build.gradle @@ -0,0 +1,9 @@ +plugins { + id 'java' + id 'org.jetbrains.kotlin.jvm' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib" + implementation "io.github.spair:imgui-java-app:1.84.1.0" +} diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/NodeEye.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/NodeEye.kt new file mode 100644 index 00000000..d7e982c7 --- /dev/null +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/NodeEye.kt @@ -0,0 +1,55 @@ +package io.github.deltacv.nodeeye + +import imgui.ImGui +import imgui.app.Application +import imgui.app.Configuration +import imgui.extension.nodeditor.NodeEditor +import imgui.extension.nodeditor.NodeEditorConfig +import imgui.extension.nodeditor.NodeEditorContext +import imgui.extension.nodeditor.flag.NodeEditorPinKind + +class NodeEye : Application() { + + private lateinit var context: NodeEditorContext + + fun start() { + val config = NodeEditorConfig() + config.settingsFile = null + + context = NodeEditorContext(config) + + launch(this) + } + + override fun configure(config: Configuration) { + config.title = "NodeEye" + } + + override fun process() { + NodeEditor.setCurrentEditor(context) + NodeEditor.begin("Editor") + + var uniqueId = 0L + + NodeEditor.beginNode(uniqueId++) + ImGui.text("Node A") + + NodeEditor.beginPin(uniqueId++, NodeEditorPinKind.Input) + ImGui.text("-> In") + NodeEditor.endPin() + + ImGui.sameLine() + + NodeEditor.beginPin(uniqueId++, NodeEditorPinKind.Output) + ImGui.text("Out ->") + NodeEditor.endPin() + NodeEditor.endNode() + + NodeEditor.end() + } + +} + +fun main() { + NodeEye().start() +} \ No newline at end of file diff --git a/imgui.ini b/imgui.ini new file mode 100644 index 00000000..24c5d21b --- /dev/null +++ b/imgui.ini @@ -0,0 +1,5 @@ +[Window][Debug##Default] +Pos=364,131 +Size=740,474 +Collapsed=0 + diff --git a/settings.gradle b/settings.gradle index 370c45da..79e21e29 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,4 +9,6 @@ pluginManagement { rootProject.name = 'EOCV-Sim' include 'TeamCode' -include 'EOCV-Sim' \ No newline at end of file +include 'EOCV-Sim' +include 'NodeEye' + From b5a7c78e58ef5a51ee453d32813b5871a6447196 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Sun, 5 Sep 2021 18:06:25 -0600 Subject: [PATCH 09/56] Work on links, nodes and attributes --- .../io/github/deltacv/nodeeye/NodeEye.kt | 69 ++++++++++++------- .../io/github/deltacv/nodeeye/id/IdElement.kt | 18 +++++ .../deltacv/nodeeye/id/IdElementContainer.kt | 55 +++++++++++++++ .../deltacv/nodeeye/id/MutuallyExclusive.kt | 11 +++ .../io/github/deltacv/nodeeye/node/Link.kt | 23 +++++++ .../io/github/deltacv/nodeeye/node/Node.kt | 24 +++++++ .../deltacv/nodeeye/node/OutputTestNode.kt | 40 +++++++++++ imgui.ini | 10 +++ 8 files changed, 227 insertions(+), 23 deletions(-) create mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/IdElement.kt create mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/IdElementContainer.kt create mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/MutuallyExclusive.kt create mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Link.kt create mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Node.kt create mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/OutputTestNode.kt diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/NodeEye.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/NodeEye.kt index d7e982c7..d5756382 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/NodeEye.kt +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/NodeEye.kt @@ -3,22 +3,29 @@ package io.github.deltacv.nodeeye import imgui.ImGui import imgui.app.Application import imgui.app.Configuration -import imgui.extension.nodeditor.NodeEditor -import imgui.extension.nodeditor.NodeEditorConfig -import imgui.extension.nodeditor.NodeEditorContext -import imgui.extension.nodeditor.flag.NodeEditorPinKind +import imgui.extension.imnodes.ImNodes +import imgui.type.ImInt +import io.github.deltacv.nodeeye.node.Link +import io.github.deltacv.nodeeye.node.Node +import io.github.deltacv.nodeeye.node.OutputTestNode +import io.github.deltacv.nodeeye.node.InputTestNode class NodeEye : Application() { - private lateinit var context: NodeEditorContext - fun start() { - val config = NodeEditorConfig() - config.settingsFile = null + ImNodes.createContext() + + val a = InputTestNode() + a.enable() - context = NodeEditorContext(config) + val b = OutputTestNode() + b.enable() + + val c = OutputTestNode() + c.enable() launch(this) + ImNodes.destroyContext() } override fun configure(config: Configuration) { @@ -26,26 +33,42 @@ class NodeEye : Application() { } override fun process() { - NodeEditor.setCurrentEditor(context) - NodeEditor.begin("Editor") + ImGui.begin("Editor") + + ImNodes.beginNodeEditor() - var uniqueId = 0L + for(node in Node.nodes) { + node.draw() + } + for(link in Link.links) { + link.draw() + } + + ImNodes.endNodeEditor() + checkLinkCreated() + + ImGui.end() + } - NodeEditor.beginNode(uniqueId++) - ImGui.text("Node A") + private val startAttr = ImInt() + private val endAttr = ImInt() - NodeEditor.beginPin(uniqueId++, NodeEditorPinKind.Input) - ImGui.text("-> In") - NodeEditor.endPin() + private fun checkLinkCreated() { + if(ImNodes.isLinkCreated(startAttr, endAttr)) { + val start = startAttr.get() + val end = endAttr.get() - ImGui.sameLine() + for(link in Link.links) { + if(link.a == start || link.a == end || link.b == start || link.b == end) { + link.delete() + break + } + } - NodeEditor.beginPin(uniqueId++, NodeEditorPinKind.Output) - ImGui.text("Out ->") - NodeEditor.endPin() - NodeEditor.endNode() + Link(start, end).enable() - NodeEditor.end() + println("link to $start ${Node.attributes[start]} and $end ${Node.attributes[end]}") + } } } diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/IdElement.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/IdElement.kt new file mode 100644 index 00000000..a077f05d --- /dev/null +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/IdElement.kt @@ -0,0 +1,18 @@ +package io.github.deltacv.nodeeye.id + +interface IdElement { + val id: Int +} + +interface DrawableIdElement : IdElement { + + fun draw() + + fun delete() + + fun enable(): DrawableIdElement { + ::id.get() + return this + } + +} \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/IdElementContainer.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/IdElementContainer.kt new file mode 100644 index 00000000..d845116e --- /dev/null +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/IdElementContainer.kt @@ -0,0 +1,55 @@ +package io.github.deltacv.nodeeye.id + +class IdElementContainer(var nextIdCallback: ((Int) -> Unit)? = null) : Iterable { + + private val e = ArrayList() + + /** + * Note that the element positions in this list won't necessarily match their ids + */ + var elements = ArrayList() + private set + + fun nextId(element: () -> T) = lazy { + nextId(element()).value + } + + fun nextId(element: T) = lazy { + e.add(element) + elements.add(element) + + val index = e.lastIndexOf(element) + + if(nextIdCallback != null) { + nextIdCallback!!(index) + } + + index + } + + fun nextId() = lazy { + e.add(null) + + val index = e.lastIndexOf(null) + + if(nextIdCallback != null) { + nextIdCallback!!(index) + } + + index + } + + fun nextIdDontTrigger(): Int { + e.add(null) + return e.lastIndexOf(null) + } + + fun removeId(id: Int) { + elements.remove(e[id]) + e[id] = null + } + + operator fun get(id: Int) = e[id] + + override fun iterator() = elements.listIterator() +} \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/MutuallyExclusive.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/MutuallyExclusive.kt new file mode 100644 index 00000000..690bd411 --- /dev/null +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/MutuallyExclusive.kt @@ -0,0 +1,11 @@ +package io.github.deltacv.nodeeye.id + +fun mutuallyExclude(a: IdElementContainer<*>, b: IdElementContainer<*>) { + a.nextIdCallback = { + b.nextIdDontTrigger() + } + + b.nextIdCallback = { + b.nextIdDontTrigger() + } +} \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Link.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Link.kt new file mode 100644 index 00000000..1c4a006d --- /dev/null +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Link.kt @@ -0,0 +1,23 @@ +package io.github.deltacv.nodeeye.node + +import imgui.extension.imnodes.ImNodes +import io.github.deltacv.nodeeye.id.DrawableIdElement +import io.github.deltacv.nodeeye.id.IdElementContainer + +class Link(val a: Int, val b: Int) : DrawableIdElement { + + companion object { + val links = IdElementContainer() + } + + override val id by links.nextId { this } + + override fun draw() { + ImNodes.link(id, a, b) + } + + override fun delete() { + links.removeId(id) + } + +} \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Node.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Node.kt new file mode 100644 index 00000000..c5398749 --- /dev/null +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Node.kt @@ -0,0 +1,24 @@ +package io.github.deltacv.nodeeye.node + +import io.github.deltacv.nodeeye.id.DrawableIdElement +import io.github.deltacv.nodeeye.id.IdElementContainer +import io.github.deltacv.nodeeye.id.mutuallyExclude + +enum class Attribute { + INPUT, OUTPUT +} + +abstract class Node : DrawableIdElement { + + companion object { + val nodes = IdElementContainer() + val attributes = IdElementContainer() + } + + override val id by nodes.nextId { this } + + override fun delete() { + nodes.removeId(id) + } + +} \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/OutputTestNode.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/OutputTestNode.kt new file mode 100644 index 00000000..04947283 --- /dev/null +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/OutputTestNode.kt @@ -0,0 +1,40 @@ +package io.github.deltacv.nodeeye.node + +import imgui.ImGui +import imgui.extension.imnodes.ImNodes + +class OutputTestNode : Node() { + + val outputAttributeId by attributes.nextId(Attribute.OUTPUT) + + override fun draw() { + ImNodes.beginNode(id) + ImNodes.beginNodeTitleBar() + ImGui.textUnformatted("Test Node A") + ImNodes.endNodeTitleBar() + + ImNodes.beginOutputAttribute(outputAttributeId) + ImGui.text("Output Pin") + ImNodes.endOutputAttribute() + ImNodes.endNode() + } + +} + +class InputTestNode : Node() { + + val inputAttributeId by attributes.nextId(Attribute.INPUT) + + override fun draw() { + ImNodes.beginNode(id) + ImNodes.beginNodeTitleBar() + ImGui.textUnformatted("Test Node B") + ImNodes.endNodeTitleBar() + + ImNodes.beginInputAttribute(inputAttributeId) + ImGui.text("Input Pin") + ImNodes.endInputAttribute() + ImNodes.endNode() + } + +} \ No newline at end of file diff --git a/imgui.ini b/imgui.ini index 24c5d21b..8e9597cf 100644 --- a/imgui.ini +++ b/imgui.ini @@ -3,3 +3,13 @@ Pos=364,131 Size=740,474 Collapsed=0 +[Window][node editor] +Pos=-42,-22 +Size=1548,708 +Collapsed=0 + +[Window][Editor] +Pos=-7,5 +Size=1257,689 +Collapsed=0 + From 106cf0032f294bf0ef0da15652185443b1b48521 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Mon, 6 Sep 2021 00:11:09 -0600 Subject: [PATCH 10/56] Improved link handling, and attributes --- .gitignore | 2 + .../io/github/deltacv/nodeeye/NodeEye.kt | 42 ++++++++++--------- .../io/github/deltacv/nodeeye/id/IdElement.kt | 3 ++ .../deltacv/nodeeye/id/IdElementContainer.kt | 24 ++--------- .../deltacv/nodeeye/id/MutuallyExclusive.kt | 11 ----- .../io/github/deltacv/nodeeye/node/Link.kt | 10 +++++ .../io/github/deltacv/nodeeye/node/Node.kt | 18 +++++--- .../deltacv/nodeeye/node/OutputTestNode.kt | 40 ------------------ .../nodeeye/node/attribute/Attribute.kt | 39 +++++++++++++++++ .../node/attribute/vision/MatAttribute.kt | 17 ++++++++ .../nodeeye/node/vision/InputMatNode.kt | 25 +++++++++++ .../nodeeye/node/vision/OutputMatNode.kt | 25 +++++++++++ imgui.ini | 4 +- 13 files changed, 162 insertions(+), 98 deletions(-) delete mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/MutuallyExclusive.kt delete mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/OutputTestNode.kt create mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/attribute/Attribute.kt create mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/attribute/vision/MatAttribute.kt create mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/InputMatNode.kt create mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/OutputMatNode.kt diff --git a/.gitignore b/.gitignore index 0c79052f..9ba30bc2 100644 --- a/.gitignore +++ b/.gitignore @@ -109,3 +109,5 @@ fabric.properties **/build/* *.DS_Store + +**/imgui.ini \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/NodeEye.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/NodeEye.kt index d5756382..8c1ca70f 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/NodeEye.kt +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/NodeEye.kt @@ -5,24 +5,18 @@ import imgui.app.Application import imgui.app.Configuration import imgui.extension.imnodes.ImNodes import imgui.type.ImInt -import io.github.deltacv.nodeeye.node.Link -import io.github.deltacv.nodeeye.node.Node -import io.github.deltacv.nodeeye.node.OutputTestNode -import io.github.deltacv.nodeeye.node.InputTestNode +import io.github.deltacv.nodeeye.node.* +import io.github.deltacv.nodeeye.node.attribute.AttributeMode +import io.github.deltacv.nodeeye.node.vision.InputMatNode +import io.github.deltacv.nodeeye.node.vision.OutputMatNode class NodeEye : Application() { fun start() { ImNodes.createContext() - val a = InputTestNode() - a.enable() - - val b = OutputTestNode() - b.enable() - - val c = OutputTestNode() - c.enable() + InputMatNode().enable() + OutputMatNode().enable() launch(this) ImNodes.destroyContext() @@ -58,16 +52,26 @@ class NodeEye : Application() { val start = startAttr.get() val end = endAttr.get() - for(link in Link.links) { - if(link.a == start || link.a == end || link.b == start || link.b == end) { - link.delete() - break - } + val startAttrib = Node.attributes[start]!! + val endAttrib = Node.attributes[end]!! + + if(startAttrib == endAttrib) { + return // linked attributes cannot be of the same type + } + + if(!startAttrib.acceptLink(endAttrib) ||!endAttrib.acceptLink(startAttrib)) { + return // one or both of the attributes didn't accept the link, abort. } - Link(start, end).enable() + val inputLink = Link.getLinkOf( + // determines which, the start or end, of this new link is the input attribute + if(startAttrib.mode == AttributeMode.INPUT) + start + else end + ) + inputLink?.delete() // delete the existing link of the input attribute if there's any - println("link to $start ${Node.attributes[start]} and $end ${Node.attributes[end]}") + Link(start, end).enable() // create the link and enable it } } diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/IdElement.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/IdElement.kt index a077f05d..4056f289 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/IdElement.kt +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/IdElement.kt @@ -10,8 +10,11 @@ interface DrawableIdElement : IdElement { fun delete() + fun onEnable() { } + fun enable(): DrawableIdElement { ::id.get() + onEnable() return this } diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/IdElementContainer.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/IdElementContainer.kt index d845116e..f66d2cb9 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/IdElementContainer.kt +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/IdElementContainer.kt @@ -1,6 +1,6 @@ package io.github.deltacv.nodeeye.id -class IdElementContainer(var nextIdCallback: ((Int) -> Unit)? = null) : Iterable { +class IdElementContainer : Iterable { private val e = ArrayList() @@ -18,30 +18,12 @@ class IdElementContainer(var nextIdCallback: ((Int) -> Unit)? = null) : Itera e.add(element) elements.add(element) - val index = e.lastIndexOf(element) - - if(nextIdCallback != null) { - nextIdCallback!!(index) - } - - index + e.lastIndexOf(element) } fun nextId() = lazy { e.add(null) - - val index = e.lastIndexOf(null) - - if(nextIdCallback != null) { - nextIdCallback!!(index) - } - - index - } - - fun nextIdDontTrigger(): Int { - e.add(null) - return e.lastIndexOf(null) + e.lastIndexOf(null) } fun removeId(id: Int) { diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/MutuallyExclusive.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/MutuallyExclusive.kt deleted file mode 100644 index 690bd411..00000000 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/MutuallyExclusive.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.deltacv.nodeeye.id - -fun mutuallyExclude(a: IdElementContainer<*>, b: IdElementContainer<*>) { - a.nextIdCallback = { - b.nextIdDontTrigger() - } - - b.nextIdCallback = { - b.nextIdDontTrigger() - } -} \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Link.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Link.kt index 1c4a006d..7e015027 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Link.kt +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Link.kt @@ -8,6 +8,16 @@ class Link(val a: Int, val b: Int) : DrawableIdElement { companion object { val links = IdElementContainer() + + fun getLinkOf(attributeId: Int): Link? { + for(link in links) { + if(link.a == attributeId || link.b == attributeId) { + return link + } + } + + return null + } } override val id by links.nextId { this } diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Node.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Node.kt index c5398749..1359041d 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Node.kt +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Node.kt @@ -2,11 +2,7 @@ package io.github.deltacv.nodeeye.node import io.github.deltacv.nodeeye.id.DrawableIdElement import io.github.deltacv.nodeeye.id.IdElementContainer -import io.github.deltacv.nodeeye.id.mutuallyExclude - -enum class Attribute { - INPUT, OUTPUT -} +import io.github.deltacv.nodeeye.node.attribute.Attribute abstract class Node : DrawableIdElement { @@ -17,7 +13,19 @@ abstract class Node : DrawableIdElement { override val id by nodes.nextId { this } + val nodeAttributes = mutableListOf() + + fun drawAttributes() { + for(attribute in nodeAttributes) { + attribute.draw() + } + } + override fun delete() { + for(attribute in nodeAttributes) { + attribute.delete() + } + nodes.removeId(id) } diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/OutputTestNode.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/OutputTestNode.kt deleted file mode 100644 index 04947283..00000000 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/OutputTestNode.kt +++ /dev/null @@ -1,40 +0,0 @@ -package io.github.deltacv.nodeeye.node - -import imgui.ImGui -import imgui.extension.imnodes.ImNodes - -class OutputTestNode : Node() { - - val outputAttributeId by attributes.nextId(Attribute.OUTPUT) - - override fun draw() { - ImNodes.beginNode(id) - ImNodes.beginNodeTitleBar() - ImGui.textUnformatted("Test Node A") - ImNodes.endNodeTitleBar() - - ImNodes.beginOutputAttribute(outputAttributeId) - ImGui.text("Output Pin") - ImNodes.endOutputAttribute() - ImNodes.endNode() - } - -} - -class InputTestNode : Node() { - - val inputAttributeId by attributes.nextId(Attribute.INPUT) - - override fun draw() { - ImNodes.beginNode(id) - ImNodes.beginNodeTitleBar() - ImGui.textUnformatted("Test Node B") - ImNodes.endNodeTitleBar() - - ImNodes.beginInputAttribute(inputAttributeId) - ImGui.text("Input Pin") - ImNodes.endInputAttribute() - ImNodes.endNode() - } - -} \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/attribute/Attribute.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/attribute/Attribute.kt new file mode 100644 index 00000000..181e7fdd --- /dev/null +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/attribute/Attribute.kt @@ -0,0 +1,39 @@ +package io.github.deltacv.nodeeye.node.attribute + +import imgui.extension.imnodes.ImNodes +import io.github.deltacv.nodeeye.id.DrawableIdElement +import io.github.deltacv.nodeeye.node.Node + +enum class AttributeMode { INPUT, OUTPUT } + +abstract class Attribute : DrawableIdElement { + + abstract val mode: AttributeMode + + override val id by Node.attributes.nextId { this } + + abstract fun drawAttribute() + + override fun draw() { + if(mode == AttributeMode.INPUT) { + ImNodes.beginInputAttribute(id) + } else { + ImNodes.beginOutputAttribute(id) + } + + drawAttribute() + + if(mode == AttributeMode.INPUT) { + ImNodes.endInputAttribute() + } else { + ImNodes.endOutputAttribute() + } + } + + override fun delete() { + Node.attributes.removeId(id) + } + + abstract fun acceptLink(other: Attribute): Boolean + +} \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/attribute/vision/MatAttribute.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/attribute/vision/MatAttribute.kt new file mode 100644 index 00000000..7602b7f2 --- /dev/null +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/attribute/vision/MatAttribute.kt @@ -0,0 +1,17 @@ +package io.github.deltacv.nodeeye.node.attribute.vision + +import imgui.ImGui +import io.github.deltacv.nodeeye.node.attribute.Attribute +import io.github.deltacv.nodeeye.node.attribute.AttributeMode + +class MatAttribute(override val mode: AttributeMode, val name: String? = null) : Attribute() { + + override fun drawAttribute() { + ImGui.text("(Image) ${name ?: + if(mode == AttributeMode.INPUT) "input" else "output" + }") + } + + override fun acceptLink(other: Attribute) = other is MatAttribute + +} \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/InputMatNode.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/InputMatNode.kt new file mode 100644 index 00000000..3f23509d --- /dev/null +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/InputMatNode.kt @@ -0,0 +1,25 @@ +package io.github.deltacv.nodeeye.node.vision + +import imgui.ImGui +import imgui.extension.imnodes.ImNodes +import io.github.deltacv.nodeeye.node.Node +import io.github.deltacv.nodeeye.node.attribute.AttributeMode +import io.github.deltacv.nodeeye.node.attribute.vision.MatAttribute + +class InputMatNode : Node() { + + override fun onEnable() { + nodeAttributes.add(MatAttribute(AttributeMode.OUTPUT, "Input")) + } + + override fun draw() { + ImNodes.beginNode(id) + ImNodes.beginNodeTitleBar() + ImGui.textUnformatted("Pipeline Input") + ImNodes.endNodeTitleBar() + + drawAttributes() + ImNodes.endNode() + } + +} \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/OutputMatNode.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/OutputMatNode.kt new file mode 100644 index 00000000..218d30fb --- /dev/null +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/OutputMatNode.kt @@ -0,0 +1,25 @@ +package io.github.deltacv.nodeeye.node.vision + +import imgui.ImGui +import imgui.extension.imnodes.ImNodes +import io.github.deltacv.nodeeye.node.Node +import io.github.deltacv.nodeeye.node.attribute.AttributeMode +import io.github.deltacv.nodeeye.node.attribute.vision.MatAttribute + +class OutputMatNode : Node() { + + override fun onEnable() { + nodeAttributes.add(MatAttribute(AttributeMode.INPUT, "Output")) + } + + override fun draw() { + ImNodes.beginNode(id) + ImNodes.beginNodeTitleBar() + ImGui.textUnformatted("Pipeline Output") + ImNodes.endNodeTitleBar() + + drawAttributes() + ImNodes.endNode() + } + +} \ No newline at end of file diff --git a/imgui.ini b/imgui.ini index 8e9597cf..93d9749f 100644 --- a/imgui.ini +++ b/imgui.ini @@ -9,7 +9,7 @@ Size=1548,708 Collapsed=0 [Window][Editor] -Pos=-7,5 -Size=1257,689 +Pos=-5,5 +Size=1363,702 Collapsed=0 From 34b6df088346e9cff9923d50706a301c5b7822e9 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Mon, 6 Sep 2021 14:01:56 -0600 Subject: [PATCH 11/56] yes --- .../io/github/deltacv/nodeeye/NodeEye.kt | 38 +++++++++++++++++-- .../nodeeye/{node => }/attribute/Attribute.kt | 2 +- .../nodeeye/attribute/TextBoxAttribute.kt | 9 +++++ .../nodeeye/attribute/TypedAttribute.kt | 17 +++++++++ .../attribute/math/IntegerAttribute.kt | 26 +++++++++++++ .../nodeeye/attribute/vision/MatAttribute.kt | 9 +++++ .../github/deltacv/nodeeye/node/DrawNode.kt | 21 ++++++++++ .../io/github/deltacv/nodeeye/node/Link.kt | 3 ++ .../io/github/deltacv/nodeeye/node/Node.kt | 4 +- .../node/attribute/vision/MatAttribute.kt | 17 --------- .../nodeeye/node/math/SumIntegerNode.kt | 15 ++++++++ .../nodeeye/node/vision/InputMatNode.kt | 20 ++-------- .../nodeeye/node/vision/OutputMatNode.kt | 20 ++-------- imgui.ini | 4 +- 14 files changed, 148 insertions(+), 57 deletions(-) rename NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/{node => }/attribute/Attribute.kt (94%) create mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/TextBoxAttribute.kt create mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/TypedAttribute.kt create mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/math/IntegerAttribute.kt create mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/vision/MatAttribute.kt create mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/DrawNode.kt delete mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/attribute/vision/MatAttribute.kt create mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/math/SumIntegerNode.kt diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/NodeEye.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/NodeEye.kt index 8c1ca70f..01460db4 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/NodeEye.kt +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/NodeEye.kt @@ -1,24 +1,47 @@ package io.github.deltacv.nodeeye import imgui.ImGui +import imgui.ImVec2 import imgui.app.Application import imgui.app.Configuration import imgui.extension.imnodes.ImNodes +import imgui.flag.ImGuiCond +import imgui.flag.ImGuiWindowFlags import imgui.type.ImInt import io.github.deltacv.nodeeye.node.* -import io.github.deltacv.nodeeye.node.attribute.AttributeMode +import io.github.deltacv.nodeeye.attribute.AttributeMode +import io.github.deltacv.nodeeye.node.math.SumIntegerNode import io.github.deltacv.nodeeye.node.vision.InputMatNode import io.github.deltacv.nodeeye.node.vision.OutputMatNode +import org.lwjgl.BufferUtils +import org.lwjgl.glfw.GLFW.glfwGetWindowSize + class NodeEye : Application() { + private val w = BufferUtils.createIntBuffer(1) + private val h = BufferUtils.createIntBuffer(1) + + val windowSize: ImVec2 get() { + w.position(0) + h.position(0) + + glfwGetWindowSize(handle, w, h) + + return ImVec2(w.get(0).toFloat(), h.get(0).toFloat()) + } + fun start() { ImNodes.createContext() InputMatNode().enable() OutputMatNode().enable() + SumIntegerNode().enable() + SumIntegerNode().enable() + launch(this) + ImNodes.destroyContext() } @@ -27,7 +50,14 @@ class NodeEye : Application() { } override fun process() { - ImGui.begin("Editor") + ImGui.setNextWindowPos(0f, 0f, ImGuiCond.Always) + + val size = windowSize + ImGui.setNextWindowSize(size.x, size.y, ImGuiCond.Always) + + ImGui.begin("Editor", + ImGuiWindowFlags.NoResize or ImGuiWindowFlags.NoMove or ImGuiWindowFlags.NoCollapse + ) ImNodes.beginNodeEditor() @@ -55,8 +85,8 @@ class NodeEye : Application() { val startAttrib = Node.attributes[start]!! val endAttrib = Node.attributes[end]!! - if(startAttrib == endAttrib) { - return // linked attributes cannot be of the same type + if(startAttrib.mode == endAttrib.mode) { + return // linked attributes cannot be of the same mode } if(!startAttrib.acceptLink(endAttrib) ||!endAttrib.acceptLink(startAttrib)) { diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/attribute/Attribute.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/Attribute.kt similarity index 94% rename from NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/attribute/Attribute.kt rename to NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/Attribute.kt index 181e7fdd..eaa31d43 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/attribute/Attribute.kt +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/Attribute.kt @@ -1,4 +1,4 @@ -package io.github.deltacv.nodeeye.node.attribute +package io.github.deltacv.nodeeye.attribute import imgui.extension.imnodes.ImNodes import io.github.deltacv.nodeeye.id.DrawableIdElement diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/TextBoxAttribute.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/TextBoxAttribute.kt new file mode 100644 index 00000000..98439a61 --- /dev/null +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/TextBoxAttribute.kt @@ -0,0 +1,9 @@ +package io.github.deltacv.nodeeye.attribute + +abstract class TextBoxAttribute(typeName: String) : TypedAttribute(typeName) { + + override fun drawAttribute() { + super.drawAttribute() + } + +} \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/TypedAttribute.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/TypedAttribute.kt new file mode 100644 index 00000000..99884d13 --- /dev/null +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/TypedAttribute.kt @@ -0,0 +1,17 @@ +package io.github.deltacv.nodeeye.attribute + +import imgui.ImGui + +abstract class TypedAttribute(var typeName: String) : Attribute() { + + abstract var variableName: String? + + protected val finalVarName get() = variableName ?: if(mode == AttributeMode.INPUT) "Input" else "Output" + + override fun drawAttribute() { + ImGui.text("($typeName) $finalVarName") + } + + override fun acceptLink(other: Attribute) = this::class == other::class + +} \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/math/IntegerAttribute.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/math/IntegerAttribute.kt new file mode 100644 index 00000000..8c4f3f42 --- /dev/null +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/math/IntegerAttribute.kt @@ -0,0 +1,26 @@ +package io.github.deltacv.nodeeye.attribute.math + +import imgui.ImGui +import imgui.type.ImInt +import io.github.deltacv.nodeeye.attribute.AttributeMode +import io.github.deltacv.nodeeye.attribute.TypedAttribute +import io.github.deltacv.nodeeye.node.Link.Companion.hasLink + +class IntAttribute( + override val mode: AttributeMode, + override var variableName: String? = null +) : TypedAttribute("Int") { + + val value = ImInt() + + override fun drawAttribute() { + super.drawAttribute() + + if(!hasLink && mode == AttributeMode.INPUT) { + ImGui.pushItemWidth(100.0f) + ImGui.inputInt("", value) + ImGui.popItemWidth() + } + } + +} \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/vision/MatAttribute.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/vision/MatAttribute.kt new file mode 100644 index 00000000..0000b4cb --- /dev/null +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/vision/MatAttribute.kt @@ -0,0 +1,9 @@ +package io.github.deltacv.nodeeye.attribute.vision + +import io.github.deltacv.nodeeye.attribute.TypedAttribute +import io.github.deltacv.nodeeye.attribute.AttributeMode + +class MatAttribute( + override val mode: AttributeMode, + override var variableName: String? = null +) : TypedAttribute("Image") \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/DrawNode.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/DrawNode.kt new file mode 100644 index 00000000..f52902c9 --- /dev/null +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/DrawNode.kt @@ -0,0 +1,21 @@ +package io.github.deltacv.nodeeye.node + +import imgui.ImGui +import imgui.extension.imnodes.ImNodes + +abstract class DrawNode(var title: String) : Node() { + + override fun draw() { + ImNodes.beginNode(id) + ImNodes.beginNodeTitleBar() + ImGui.textUnformatted(title) + ImNodes.endNodeTitleBar() + + drawNode() + drawAttributes() + ImNodes.endNode() + } + + open fun drawNode() { } + +} \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Link.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Link.kt index 7e015027..992b8764 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Link.kt +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Link.kt @@ -1,6 +1,7 @@ package io.github.deltacv.nodeeye.node import imgui.extension.imnodes.ImNodes +import io.github.deltacv.nodeeye.attribute.Attribute import io.github.deltacv.nodeeye.id.DrawableIdElement import io.github.deltacv.nodeeye.id.IdElementContainer @@ -18,6 +19,8 @@ class Link(val a: Int, val b: Int) : DrawableIdElement { return null } + + val Attribute.hasLink get() = getLinkOf(id) != null } override val id by links.nextId { this } diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Node.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Node.kt index 1359041d..9cba8247 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Node.kt +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Node.kt @@ -1,8 +1,9 @@ package io.github.deltacv.nodeeye.node +import imgui.ImGui import io.github.deltacv.nodeeye.id.DrawableIdElement import io.github.deltacv.nodeeye.id.IdElementContainer -import io.github.deltacv.nodeeye.node.attribute.Attribute +import io.github.deltacv.nodeeye.attribute.Attribute abstract class Node : DrawableIdElement { @@ -18,6 +19,7 @@ abstract class Node : DrawableIdElement { fun drawAttributes() { for(attribute in nodeAttributes) { attribute.draw() + //ImGui.separator() TODO separate } } diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/attribute/vision/MatAttribute.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/attribute/vision/MatAttribute.kt deleted file mode 100644 index 7602b7f2..00000000 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/attribute/vision/MatAttribute.kt +++ /dev/null @@ -1,17 +0,0 @@ -package io.github.deltacv.nodeeye.node.attribute.vision - -import imgui.ImGui -import io.github.deltacv.nodeeye.node.attribute.Attribute -import io.github.deltacv.nodeeye.node.attribute.AttributeMode - -class MatAttribute(override val mode: AttributeMode, val name: String? = null) : Attribute() { - - override fun drawAttribute() { - ImGui.text("(Image) ${name ?: - if(mode == AttributeMode.INPUT) "input" else "output" - }") - } - - override fun acceptLink(other: Attribute) = other is MatAttribute - -} \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/math/SumIntegerNode.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/math/SumIntegerNode.kt new file mode 100644 index 00000000..63e8ff09 --- /dev/null +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/math/SumIntegerNode.kt @@ -0,0 +1,15 @@ +package io.github.deltacv.nodeeye.node.math + +import io.github.deltacv.nodeeye.node.DrawNode +import io.github.deltacv.nodeeye.attribute.AttributeMode +import io.github.deltacv.nodeeye.attribute.math.IntAttribute + +class SumIntegerNode : DrawNode("Sum Integer") { + + override fun onEnable() { + nodeAttributes.add(IntAttribute(AttributeMode.INPUT, "A")) + nodeAttributes.add(IntAttribute(AttributeMode.INPUT, "B")) + nodeAttributes.add(IntAttribute(AttributeMode.OUTPUT, "Result")) + } + +} \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/InputMatNode.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/InputMatNode.kt index 3f23509d..46d1a05a 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/InputMatNode.kt +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/InputMatNode.kt @@ -1,25 +1,13 @@ package io.github.deltacv.nodeeye.node.vision -import imgui.ImGui -import imgui.extension.imnodes.ImNodes -import io.github.deltacv.nodeeye.node.Node -import io.github.deltacv.nodeeye.node.attribute.AttributeMode -import io.github.deltacv.nodeeye.node.attribute.vision.MatAttribute +import io.github.deltacv.nodeeye.node.DrawNode +import io.github.deltacv.nodeeye.attribute.AttributeMode +import io.github.deltacv.nodeeye.attribute.vision.MatAttribute -class InputMatNode : Node() { +class InputMatNode : DrawNode("Pipeline Input") { override fun onEnable() { nodeAttributes.add(MatAttribute(AttributeMode.OUTPUT, "Input")) } - override fun draw() { - ImNodes.beginNode(id) - ImNodes.beginNodeTitleBar() - ImGui.textUnformatted("Pipeline Input") - ImNodes.endNodeTitleBar() - - drawAttributes() - ImNodes.endNode() - } - } \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/OutputMatNode.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/OutputMatNode.kt index 218d30fb..ae49f26c 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/OutputMatNode.kt +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/OutputMatNode.kt @@ -1,25 +1,13 @@ package io.github.deltacv.nodeeye.node.vision -import imgui.ImGui -import imgui.extension.imnodes.ImNodes -import io.github.deltacv.nodeeye.node.Node -import io.github.deltacv.nodeeye.node.attribute.AttributeMode -import io.github.deltacv.nodeeye.node.attribute.vision.MatAttribute +import io.github.deltacv.nodeeye.node.DrawNode +import io.github.deltacv.nodeeye.attribute.AttributeMode +import io.github.deltacv.nodeeye.attribute.vision.MatAttribute -class OutputMatNode : Node() { +class OutputMatNode : DrawNode("Pipeline Output") { override fun onEnable() { nodeAttributes.add(MatAttribute(AttributeMode.INPUT, "Output")) } - override fun draw() { - ImNodes.beginNode(id) - ImNodes.beginNodeTitleBar() - ImGui.textUnformatted("Pipeline Output") - ImNodes.endNodeTitleBar() - - drawAttributes() - ImNodes.endNode() - } - } \ No newline at end of file diff --git a/imgui.ini b/imgui.ini index 93d9749f..a483ddc0 100644 --- a/imgui.ini +++ b/imgui.ini @@ -9,7 +9,7 @@ Size=1548,708 Collapsed=0 [Window][Editor] -Pos=-5,5 -Size=1363,702 +Pos=0,0 +Size=1280,749 Collapsed=0 From 10eafc41e9e4d46af1b666855913281bb76dc152 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Tue, 7 Sep 2021 13:25:26 -0600 Subject: [PATCH 12/56] Work on node and links selection & deletion --- .../io/github/deltacv/nodeeye/EasyVision.kt | 71 +++++++++++ .../io/github/deltacv/nodeeye/NodeEye.kt | 112 ------------------ .../deltacv/nodeeye/attribute/Attribute.kt | 3 + .../attribute/math/IntegerAttribute.kt | 2 +- .../github/deltacv/nodeeye/node/DrawNode.kt | 10 +- .../io/github/deltacv/nodeeye/node/Node.kt | 28 +++-- .../github/deltacv/nodeeye/node/NodeEditor.kt | 103 ++++++++++++++++ .../nodeeye/node/math/SumIntegerNode.kt | 8 +- .../nodeeye/node/vision/InputMatNode.kt | 5 +- .../nodeeye/node/vision/OutputMatNode.kt | 4 +- imgui.ini | 2 +- 11 files changed, 213 insertions(+), 135 deletions(-) create mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/EasyVision.kt delete mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/NodeEye.kt create mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/NodeEditor.kt diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/EasyVision.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/EasyVision.kt new file mode 100644 index 00000000..9ccdef31 --- /dev/null +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/EasyVision.kt @@ -0,0 +1,71 @@ +package io.github.deltacv.nodeeye + +import imgui.ImGui +import imgui.ImVec2 +import imgui.app.Application +import imgui.app.Configuration +import imgui.flag.ImGuiCond +import imgui.flag.ImGuiWindowFlags +import io.github.deltacv.nodeeye.node.* + +import io.github.deltacv.nodeeye.node.math.SumIntegerNode +import io.github.deltacv.nodeeye.node.vision.InputMatNode +import io.github.deltacv.nodeeye.node.vision.OutputMatNode + +import org.lwjgl.BufferUtils +import org.lwjgl.glfw.GLFW.glfwGetWindowSize + +class NodeEye : Application() { + + private val w = BufferUtils.createIntBuffer(1) + private val h = BufferUtils.createIntBuffer(1) + + val windowSize: ImVec2 get() { + w.position(0) + h.position(0) + + glfwGetWindowSize(handle, w, h) + + return ImVec2(w.get(0).toFloat(), h.get(0).toFloat()) + } + + val editor = NodeEditor() + + override fun configure(config: Configuration) { + config.title = "NodeEye" + } + + fun start() { + editor.init() + + InputMatNode().enable() + OutputMatNode().enable() + + SumIntegerNode().enable() + SumIntegerNode().enable() + + launch(this) + + editor.destroy() + } + + override fun process() { + ImGui.setNextWindowPos(0f, 0f, ImGuiCond.Always) + + val size = windowSize + ImGui.setNextWindowSize(size.x, size.y, ImGuiCond.Always) + + ImGui.begin("Editor", + ImGuiWindowFlags.NoResize or ImGuiWindowFlags.NoMove or ImGuiWindowFlags.NoCollapse + ) + + editor.draw() + + ImGui.end() + } + +} + +fun main() { + NodeEye().start() +} \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/NodeEye.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/NodeEye.kt deleted file mode 100644 index 01460db4..00000000 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/NodeEye.kt +++ /dev/null @@ -1,112 +0,0 @@ -package io.github.deltacv.nodeeye - -import imgui.ImGui -import imgui.ImVec2 -import imgui.app.Application -import imgui.app.Configuration -import imgui.extension.imnodes.ImNodes -import imgui.flag.ImGuiCond -import imgui.flag.ImGuiWindowFlags -import imgui.type.ImInt -import io.github.deltacv.nodeeye.node.* -import io.github.deltacv.nodeeye.attribute.AttributeMode -import io.github.deltacv.nodeeye.node.math.SumIntegerNode -import io.github.deltacv.nodeeye.node.vision.InputMatNode -import io.github.deltacv.nodeeye.node.vision.OutputMatNode - -import org.lwjgl.BufferUtils -import org.lwjgl.glfw.GLFW.glfwGetWindowSize - -class NodeEye : Application() { - - private val w = BufferUtils.createIntBuffer(1) - private val h = BufferUtils.createIntBuffer(1) - - val windowSize: ImVec2 get() { - w.position(0) - h.position(0) - - glfwGetWindowSize(handle, w, h) - - return ImVec2(w.get(0).toFloat(), h.get(0).toFloat()) - } - - fun start() { - ImNodes.createContext() - - InputMatNode().enable() - OutputMatNode().enable() - - SumIntegerNode().enable() - SumIntegerNode().enable() - - launch(this) - - ImNodes.destroyContext() - } - - override fun configure(config: Configuration) { - config.title = "NodeEye" - } - - override fun process() { - ImGui.setNextWindowPos(0f, 0f, ImGuiCond.Always) - - val size = windowSize - ImGui.setNextWindowSize(size.x, size.y, ImGuiCond.Always) - - ImGui.begin("Editor", - ImGuiWindowFlags.NoResize or ImGuiWindowFlags.NoMove or ImGuiWindowFlags.NoCollapse - ) - - ImNodes.beginNodeEditor() - - for(node in Node.nodes) { - node.draw() - } - for(link in Link.links) { - link.draw() - } - - ImNodes.endNodeEditor() - checkLinkCreated() - - ImGui.end() - } - - private val startAttr = ImInt() - private val endAttr = ImInt() - - private fun checkLinkCreated() { - if(ImNodes.isLinkCreated(startAttr, endAttr)) { - val start = startAttr.get() - val end = endAttr.get() - - val startAttrib = Node.attributes[start]!! - val endAttrib = Node.attributes[end]!! - - if(startAttrib.mode == endAttrib.mode) { - return // linked attributes cannot be of the same mode - } - - if(!startAttrib.acceptLink(endAttrib) ||!endAttrib.acceptLink(startAttrib)) { - return // one or both of the attributes didn't accept the link, abort. - } - - val inputLink = Link.getLinkOf( - // determines which, the start or end, of this new link is the input attribute - if(startAttrib.mode == AttributeMode.INPUT) - start - else end - ) - inputLink?.delete() // delete the existing link of the input attribute if there's any - - Link(start, end).enable() // create the link and enable it - } - } - -} - -fun main() { - NodeEye().start() -} \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/Attribute.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/Attribute.kt index eaa31d43..36094f76 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/Attribute.kt +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/Attribute.kt @@ -12,6 +12,9 @@ abstract class Attribute : DrawableIdElement { override val id by Node.attributes.nextId { this } + var parentNode: Node? = null + internal set + abstract fun drawAttribute() override fun draw() { diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/math/IntegerAttribute.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/math/IntegerAttribute.kt index 8c4f3f42..baa7ed7e 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/math/IntegerAttribute.kt +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/math/IntegerAttribute.kt @@ -17,7 +17,7 @@ class IntAttribute( super.drawAttribute() if(!hasLink && mode == AttributeMode.INPUT) { - ImGui.pushItemWidth(100.0f) + ImGui.pushItemWidth(110.0f) ImGui.inputInt("", value) ImGui.popItemWidth() } diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/DrawNode.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/DrawNode.kt index f52902c9..d8734dbc 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/DrawNode.kt +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/DrawNode.kt @@ -3,13 +3,15 @@ package io.github.deltacv.nodeeye.node import imgui.ImGui import imgui.extension.imnodes.ImNodes -abstract class DrawNode(var title: String) : Node() { +abstract class DrawNode(var title: String? = null, allowDelete: Boolean = true) : Node(allowDelete) { override fun draw() { ImNodes.beginNode(id) - ImNodes.beginNodeTitleBar() - ImGui.textUnformatted(title) - ImNodes.endNodeTitleBar() + if(title != null) { + ImNodes.beginNodeTitleBar() + ImGui.textUnformatted(title!!) + ImNodes.endNodeTitleBar() + } drawNode() drawAttributes() diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Node.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Node.kt index 9cba8247..04df3e6d 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Node.kt +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Node.kt @@ -4,31 +4,43 @@ import imgui.ImGui import io.github.deltacv.nodeeye.id.DrawableIdElement import io.github.deltacv.nodeeye.id.IdElementContainer import io.github.deltacv.nodeeye.attribute.Attribute +import io.github.deltacv.nodeeye.attribute.AttributeMode -abstract class Node : DrawableIdElement { +abstract class Node(protected var allowDelete: Boolean = true) : DrawableIdElement { companion object { val nodes = IdElementContainer() val attributes = IdElementContainer() + + @JvmStatic protected val INPUT = AttributeMode.INPUT + @JvmStatic protected val OUTPUT = AttributeMode.OUTPUT } override val id by nodes.nextId { this } val nodeAttributes = mutableListOf() - fun drawAttributes() { - for(attribute in nodeAttributes) { + protected fun drawAttributes() { + for((i, attribute) in nodeAttributes.withIndex()) { + attribute.parentNode = this attribute.draw() - //ImGui.separator() TODO separate + + if(i < nodeAttributes.size - 1) { + ImGui.newLine() // make a new blank line if this isn't the last attribute + } } } override fun delete() { - for(attribute in nodeAttributes) { - attribute.delete() - } + if(allowDelete) { + for (attribute in nodeAttributes) { + attribute.delete() + } - nodes.removeId(id) + nodes.removeId(id) + } } + operator fun Attribute.unaryPlus() = nodeAttributes.add(this) + } \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/NodeEditor.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/NodeEditor.kt new file mode 100644 index 00000000..0a84bd0e --- /dev/null +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/NodeEditor.kt @@ -0,0 +1,103 @@ +package io.github.deltacv.nodeeye.node + +import imgui.ImGui +import imgui.extension.imnodes.ImNodes +import imgui.flag.ImGuiKey +import imgui.flag.ImGuiMouseButton +import imgui.type.ImInt +import io.github.deltacv.nodeeye.attribute.AttributeMode + +class NodeEditor { + + fun init() { + ImNodes.createContext() + } + + fun draw() { + ImNodes.beginNodeEditor() + + for(node in Node.nodes) { + node.draw() + } + for(link in Link.links) { + link.draw() + } + + ImNodes.endNodeEditor() + + handleDeleteLink() + handleCreateLink() + handleDeleteSelection() + } + + private val startAttr = ImInt() + private val endAttr = ImInt() + + private fun handleCreateLink() { + if(ImNodes.isLinkCreated(startAttr, endAttr)) { + val start = startAttr.get() + val end = endAttr.get() + + val startAttrib = Node.attributes[start]!! + val endAttrib = Node.attributes[end]!! + + val input = if(startAttrib.mode == AttributeMode.INPUT) start else end + + val inputAttrib = Node.attributes[input]!! + val outputAttrib = if(startAttrib.mode == AttributeMode.OUTPUT) start else end + + if(startAttrib.mode == endAttrib.mode) { + return // linked attributes cannot be of the same mode + } + + if(!startAttrib.acceptLink(endAttrib) ||!endAttrib.acceptLink(startAttrib)) { + return // one or both of the attributes didn't accept the link, abort. + } + + if(startAttrib.parentNode == endAttrib.parentNode) { + return // we can't link a node to itself! + } + + val inputLink = Link.getLinkOf(input) + inputLink?.delete() // delete the existing link of the input attribute if there's any + + Link(start, end).enable() // create the link and enable it + } + } + + fun handleDeleteLink() { + val hoveredId = ImNodes.getHoveredLink() + + if(ImGui.isMouseClicked(ImGuiMouseButton.Right) && hoveredId >= 0) { + val hoveredLink = Link.links[hoveredId] + hoveredLink?.delete() + } + } + + fun handleDeleteSelection() { + if(ImGui.isKeyReleased(ImGuiKey.Delete)) { + if(ImNodes.numSelectedNodes() > 0) { + val selectedNodes = IntArray(ImNodes.numSelectedNodes()) + ImNodes.getSelectedNodes(selectedNodes) + + for(node in selectedNodes) { + Node.nodes[node]?.delete() + } + } + + if(ImNodes.numSelectedLinks() > 0) { + val selectedLinks = IntArray(ImNodes.numSelectedLinks()) + ImNodes.getSelectedLinks(selectedLinks) + + for(link in selectedLinks) { + Link.links[link]?.delete() + } + } + } + } + + fun destroy() { + ImNodes.destroyContext() + } + +} \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/math/SumIntegerNode.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/math/SumIntegerNode.kt index 63e8ff09..a83b5ad7 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/math/SumIntegerNode.kt +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/math/SumIntegerNode.kt @@ -1,15 +1,15 @@ package io.github.deltacv.nodeeye.node.math import io.github.deltacv.nodeeye.node.DrawNode -import io.github.deltacv.nodeeye.attribute.AttributeMode import io.github.deltacv.nodeeye.attribute.math.IntAttribute class SumIntegerNode : DrawNode("Sum Integer") { override fun onEnable() { - nodeAttributes.add(IntAttribute(AttributeMode.INPUT, "A")) - nodeAttributes.add(IntAttribute(AttributeMode.INPUT, "B")) - nodeAttributes.add(IntAttribute(AttributeMode.OUTPUT, "Result")) + + IntAttribute(INPUT, "A") + + IntAttribute(INPUT, "B") + + + IntAttribute(OUTPUT, "Result") } } \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/InputMatNode.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/InputMatNode.kt index 46d1a05a..e0e7f70a 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/InputMatNode.kt +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/InputMatNode.kt @@ -1,13 +1,12 @@ package io.github.deltacv.nodeeye.node.vision import io.github.deltacv.nodeeye.node.DrawNode -import io.github.deltacv.nodeeye.attribute.AttributeMode import io.github.deltacv.nodeeye.attribute.vision.MatAttribute -class InputMatNode : DrawNode("Pipeline Input") { +class InputMatNode : DrawNode("Pipeline Input", allowDelete = false) { override fun onEnable() { - nodeAttributes.add(MatAttribute(AttributeMode.OUTPUT, "Input")) + + MatAttribute(OUTPUT, "Input") } } \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/OutputMatNode.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/OutputMatNode.kt index ae49f26c..248d02f2 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/OutputMatNode.kt +++ b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/OutputMatNode.kt @@ -4,10 +4,10 @@ import io.github.deltacv.nodeeye.node.DrawNode import io.github.deltacv.nodeeye.attribute.AttributeMode import io.github.deltacv.nodeeye.attribute.vision.MatAttribute -class OutputMatNode : DrawNode("Pipeline Output") { +class OutputMatNode : DrawNode("Pipeline Output", allowDelete = false) { override fun onEnable() { - nodeAttributes.add(MatAttribute(AttributeMode.INPUT, "Output")) + + MatAttribute(INPUT, "Output") } } \ No newline at end of file diff --git a/imgui.ini b/imgui.ini index a483ddc0..e96823e0 100644 --- a/imgui.ini +++ b/imgui.ini @@ -10,6 +10,6 @@ Collapsed=0 [Window][Editor] Pos=0,0 -Size=1280,749 +Size=1366,705 Collapsed=0 From 81c01f81b16be78546105688019bed4a9b000085 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Tue, 7 Sep 2021 19:30:38 -0600 Subject: [PATCH 13/56] Fix delete key input by attaching to underlying callback --- .github/ISSUE_TEMPLATE/bug_report.md | 62 +- .github/ISSUE_TEMPLATE/feature_request.md | 40 +- .github/PULL_REQUEST_TEMPLATE.yml | 30 +- .github/dependabot.yml | 24 +- .github/workflows/build_ci.yml | 98 +- .../workflows/gradle-wrapper-validation.yml | 56 +- .github/workflows/release_ci.yml | 94 +- .github/workflows/update-gradle-wrapper.yml | 36 +- .gitignore | 224 +-- .replit | 2 +- .run/Run Simulator.run.xml | 44 +- EOCV-Sim/build.gradle | 170 +-- .../github/serivesmejia/eocvsim/EOCVSim.kt | 768 +++++----- .../com/github/serivesmejia/eocvsim/Main.kt | 136 +- .../serivesmejia/eocvsim/config/Config.java | 120 +- .../eocvsim/config/ConfigLoader.java | 146 +- .../eocvsim/config/ConfigManager.java | 122 +- .../eocvsim/gui/DialogFactory.java | 416 +++--- .../github/serivesmejia/eocvsim/gui/Icons.kt | 286 ++-- .../serivesmejia/eocvsim/gui/Visualizer.java | 1184 +++++++-------- .../eocvsim/gui/component/ImageX.java | 176 +-- .../eocvsim/gui/component/PopupX.kt | 198 +-- .../eocvsim/gui/component/SliderX.kt | 142 +- .../eocvsim/gui/component/Viewport.java | 288 ++-- .../gui/component/input/EnumComboBox.kt | 156 +- .../gui/component/input/FileSelector.kt | 150 +- .../eocvsim/gui/component/input/SizeFields.kt | 280 ++-- .../gui/component/tuner/ColorPicker.kt | 230 +-- .../component/tuner/TunableFieldPanel.java | 436 +++--- .../tuner/TunableFieldPanelConfig.kt | 660 ++++----- .../tuner/TunableFieldPanelOptions.kt | 388 ++--- .../tuner/element/TunableComboBox.java | 132 +- .../component/tuner/element/TunableSlider.kt | 150 +- .../tuner/element/TunableTextField.java | 382 ++--- .../component/visualizer/CreateSourcePanel.kt | 130 +- .../visualizer/InputSourceDropTarget.kt | 84 +- .../visualizer/SourceSelectorPanel.kt | 358 ++--- .../component/visualizer/TelemetryPanel.kt | 200 +-- .../gui/component/visualizer/TopMenuBar.kt | 312 ++-- .../pipeline/PipelineSelectorButtonsPanel.kt | 218 +-- .../pipeline/PipelineSelectorPanel.kt | 300 ++-- .../eocvsim/gui/dialog/About.java | 358 ++--- .../eocvsim/gui/dialog/Configuration.java | 336 ++--- .../eocvsim/gui/dialog/FileAlreadyExists.java | 194 +-- .../serivesmejia/eocvsim/gui/dialog/Output.kt | 478 +++--- .../eocvsim/gui/dialog/SplashScreen.kt | 112 +- .../gui/dialog/component/OutputPanel.kt | 222 +-- .../gui/dialog/source/CreateCameraSource.java | 566 ++++---- .../gui/dialog/source/CreateImageSource.java | 422 +++--- .../gui/dialog/source/CreateSource.java | 224 +-- .../gui/dialog/source/CreateVideoSource.java | 442 +++--- .../serivesmejia/eocvsim/gui/theme/Theme.java | 112 +- .../eocvsim/gui/theme/ThemeInstaller.java | 14 +- .../eocvsim/gui/util/GuiUtil.java | 490 +++---- .../eocvsim/gui/util/MatPoster.java | 430 +++--- .../eocvsim/gui/util/ReflectTaskbar.kt | 64 +- .../gui/util/ValidCharactersDocumentFilter.kt | 136 +- .../eocvsim/gui/util/extension/SwingExt.kt | 88 +- .../gui/util/icon/PipelineListIconRenderer.kt | 90 +- .../util/icon/SourcesListIconRenderer.java | 168 +-- .../eocvsim/input/InputSource.java | 178 +-- .../eocvsim/input/InputSourceLoader.java | 366 ++--- .../eocvsim/input/InputSourceManager.java | 584 ++++---- .../eocvsim/input/SourceType.java | 162 +-- .../eocvsim/input/source/CameraSource.java | 402 +++--- .../eocvsim/input/source/ImageSource.java | 354 ++--- .../eocvsim/input/source/VideoSource.java | 434 +++--- .../eocvsim/output/VideoRecordingSession.kt | 318 ++-- .../eocvsim/pipeline/DefaultPipeline.java | 166 +-- .../eocvsim/pipeline/PipelineManager.kt | 1284 ++++++++--------- .../eocvsim/pipeline/PipelineScanner.kt | 124 +- .../compiler/CompiledPipelineManager.kt | 522 +++---- .../pipeline/compiler/PipelineClassLoader.kt | 204 +-- .../pipeline/compiler/PipelineCompiler.kt | 296 ++-- .../compiler/PipelineStandardFileManager.kt | 134 +- .../pipeline/util/PipelineExceptionTracker.kt | 342 ++--- .../eocvsim/pipeline/util/PipelineSnapshot.kt | 264 ++-- .../eocvsim/tuner/TunableField.java | 294 ++-- .../eocvsim/tuner/TunableFieldAcceptor.kt | 54 +- .../tuner/TunableFieldAcceptorManager.kt | 54 +- .../eocvsim/tuner/TunerManager.java | 342 ++--- .../eocvsim/tuner/field/BooleanField.java | 210 +-- .../eocvsim/tuner/field/EnumField.kt | 128 +- .../eocvsim/tuner/field/NumericField.java | 178 +-- .../eocvsim/tuner/field/StringField.java | 196 +-- .../eocvsim/tuner/field/cv/PointField.java | 224 +-- .../eocvsim/tuner/field/cv/RectField.kt | 200 +-- .../eocvsim/tuner/field/cv/ScalarField.java | 222 +-- .../tuner/field/numeric/DoubleField.java | 132 +- .../tuner/field/numeric/FloatField.java | 130 +- .../tuner/field/numeric/IntegerField.java | 126 +- .../tuner/field/numeric/LongField.java | 126 +- .../scanner/AnnotatedTunableFieldScanner.kt | 188 +-- .../tuner/scanner/RegisterTunableField.java | 66 +- .../scanner/RegisterTunableFieldAcceptor.java | 74 +- .../serivesmejia/eocvsim/util/CvUtil.java | 362 ++--- .../serivesmejia/eocvsim/util/FileFilters.kt | 74 +- .../github/serivesmejia/eocvsim/util/Log.java | 552 +++---- .../eocvsim/util/ReflectUtil.java | 102 +- .../serivesmejia/eocvsim/util/StrUtil.java | 170 +-- .../serivesmejia/eocvsim/util/SysUtil.java | 770 +++++----- .../compiler/DelegatingStandardFileManager.kt | 110 +- .../eocvsim/util/compiler/JarPacker.kt | 154 +- .../eocvsim/util/event/EventHandler.kt | 284 ++-- .../eocvsim/util/event/EventListener.kt | 82 +- .../exception/MaxActiveContextsException.kt | 4 +- .../util/exception/handling/CrashReport.kt | 236 +-- .../EOCVSimUncaughtExceptionHandler.kt | 112 +- .../eocvsim/util/extension/CvExt.kt | 102 +- .../eocvsim/util/extension/FileExt.kt | 58 +- .../eocvsim/util/extension/NumberExt.kt | 66 +- .../eocvsim/util/extension/StrExt.kt | 60 +- .../eocvsim/util/fps/FpsCounter.kt | 114 +- .../eocvsim/util/fps/FpsLimiter.kt | 82 +- .../util/image/BufferedImageRecycler.java | 214 +-- .../image/DynamicBufferedImageRecycler.java | 154 +- .../eocvsim/util/io/EOCVSimFolder.kt | 30 +- .../eocvsim/util/io/FileWatcher.kt | 152 +- .../serivesmejia/eocvsim/util/io/Lock.kt | 150 +- .../eocvsim/workspace/WorkspaceManager.kt | 382 ++--- .../workspace/config/WorkspaceConfig.java | 70 +- .../workspace/config/WorkspaceConfigLoader.kt | 62 +- .../eocvsim/workspace/util/VSCodeLauncher.kt | 104 +- .../workspace/util/WorkspaceTemplate.kt | 78 +- .../util/template/DefaultWorkspaceTemplate.kt | 120 +- .../util/template/GradleWorkspaceTemplate.kt | 152 +- .../qualcomm/robotcore/util/ElapsedTime.java | 530 +++---- .../robotcore/util/MovingStatistics.java | 248 ++-- .../com/qualcomm/robotcore/util/Range.java | 314 ++-- .../qualcomm/robotcore/util/Statistics.java | 278 ++-- .../ftc/robotcore/external/Func.java | 14 +- .../ftc/robotcore/external/Telemetry.java | 532 +++---- .../robotcore/external/function/Consumer.java | 76 +- .../collections/EvictingBlockingQueue.java | 406 +++--- .../ftc/robotcore/internal/system/Assert.java | 230 +-- .../org/openftc/easyopencv/MatRecycler.java | 226 +-- .../openftc/easyopencv/OpenCvPipeline.java | 24 +- .../org/openftc/easyopencv/OpenCvTracker.java | 68 +- .../easyopencv/OpenCvTrackerApiPipeline.java | 140 +- .../easyopencv/TimestampedOpenCvPipeline.java | 82 +- .../easyopencv/TimestampedPipelineHandler.kt | 70 +- EOCV-Sim/src/main/resources/contributors.txt | 10 +- .../src/main/resources/opensourcelibs.txt | 14 +- .../serivesmejia/eocvsim/test/CoreTests.kt | 76 +- {NodeEye => EasyVision}/build.gradle | 18 +- .../github/deltacv/easyvision/EasyVision.kt | 91 ++ .../easyvision}/attribute/Attribute.kt | 82 +- .../easyvision}/attribute/TextBoxAttribute.kt | 16 +- .../easyvision}/attribute/TypedAttribute.kt | 32 +- .../attribute/math/IntegerAttribute.kt | 50 +- .../attribute/vision/MatAttribute.kt | 9 + .../deltacv/easyvision}/id/IdElement.kt | 40 +- .../easyvision}/id/IdElementContainer.kt | 72 +- .../deltacv/easyvision}/node/DrawNode.kt | 44 +- .../github/deltacv/easyvision}/node/Link.kt | 70 +- .../github/deltacv/easyvision}/node/Node.kt | 101 +- .../deltacv/easyvision}/node/NodeEditor.kt | 204 +-- .../easyvision}/node/math/SumIntegerNode.kt | 28 +- .../easyvision/node/vision/InputMatNode.kt | 12 + .../easyvision/node/vision/OutputMatNode.kt | 12 + LICENSE | 42 +- .../io/github/deltacv/nodeeye/EasyVision.kt | 71 - .../nodeeye/attribute/vision/MatAttribute.kt | 9 - .../nodeeye/node/vision/InputMatNode.kt | 12 - .../nodeeye/node/vision/OutputMatNode.kt | 13 - README.md | 616 ++++---- TeamCode/build.gradle | 34 +- .../ftc/teamcode/SimpleThresholdPipeline.java | 342 ++--- .../SkystoneDeterminationPipeline.java | 614 ++++---- .../ftc/teamcode/StageSwitchingPipeline.java | 266 ++-- .../StoneOrientationAnalysisPipeline.java | 932 ++++++------ USAGE.md | 840 +++++------ build.common.gradle | 24 +- build.gradle | 122 +- gradle/wrapper/gradle-wrapper.properties | 10 +- gradlew | 370 ++--- gradlew.bat | 178 +-- imgui.ini | 2 +- jitpack.yml | 12 +- settings.gradle | 2 +- test-logging.gradle | 74 +- 181 files changed, 18724 insertions(+), 18694 deletions(-) rename {NodeEye => EasyVision}/build.gradle (95%) create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt rename {NodeEye/src/main/kotlin/io/github/deltacv/nodeeye => EasyVision/src/main/kotlin/io/github/deltacv/easyvision}/attribute/Attribute.kt (80%) rename {NodeEye/src/main/kotlin/io/github/deltacv/nodeeye => EasyVision/src/main/kotlin/io/github/deltacv/easyvision}/attribute/TextBoxAttribute.kt (73%) rename {NodeEye/src/main/kotlin/io/github/deltacv/nodeeye => EasyVision/src/main/kotlin/io/github/deltacv/easyvision}/attribute/TypedAttribute.kt (86%) rename {NodeEye/src/main/kotlin/io/github/deltacv/nodeeye => EasyVision/src/main/kotlin/io/github/deltacv/easyvision}/attribute/math/IntegerAttribute.kt (63%) create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/MatAttribute.kt rename {NodeEye/src/main/kotlin/io/github/deltacv/nodeeye => EasyVision/src/main/kotlin/io/github/deltacv/easyvision}/id/IdElement.kt (80%) rename {NodeEye/src/main/kotlin/io/github/deltacv/nodeeye => EasyVision/src/main/kotlin/io/github/deltacv/easyvision}/id/IdElementContainer.kt (90%) rename {NodeEye/src/main/kotlin/io/github/deltacv/nodeeye => EasyVision/src/main/kotlin/io/github/deltacv/easyvision}/node/DrawNode.kt (89%) rename {NodeEye/src/main/kotlin/io/github/deltacv/nodeeye => EasyVision/src/main/kotlin/io/github/deltacv/easyvision}/node/Link.kt (72%) rename {NodeEye/src/main/kotlin/io/github/deltacv/nodeeye => EasyVision/src/main/kotlin/io/github/deltacv/easyvision}/node/Node.kt (54%) rename {NodeEye/src/main/kotlin/io/github/deltacv/nodeeye => EasyVision/src/main/kotlin/io/github/deltacv/easyvision}/node/NodeEditor.kt (86%) rename {NodeEye/src/main/kotlin/io/github/deltacv/nodeeye => EasyVision/src/main/kotlin/io/github/deltacv/easyvision}/node/math/SumIntegerNode.kt (53%) create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/InputMatNode.kt create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/OutputMatNode.kt delete mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/EasyVision.kt delete mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/vision/MatAttribute.kt delete mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/InputMatNode.kt delete mode 100644 NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/OutputMatNode.kt diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index e9a0d8fb..7bc3f503 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,31 +1,31 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - - OS: [e.g. Windows 10] - - Version [e.g. 2.0.0] - -**Additional context** -Add any other context about the problem here. +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. Windows 10] + - Version [e.g. 2.0.0] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index bbcbbe7d..72718d5a 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,20 +1,20 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.yml b/.github/PULL_REQUEST_TEMPLATE.yml index d743bcf2..16a414ae 100644 --- a/.github/PULL_REQUEST_TEMPLATE.yml +++ b/.github/PULL_REQUEST_TEMPLATE.yml @@ -1,15 +1,15 @@ -# Pull Requests - -Please note that we accept pull requests from anyone, but that does not mean it will be merged. - -## What kind of change does this PR introduce? -* Fix -* Feature -* Codestyle -* Refactor -* Other - -## Did this PR introduce a breaking change? -_A breaking change includes anything that breaks backwards compatibility either at compile or run time._ -* Yes, please list breaking changes -* No +# Pull Requests + +Please note that we accept pull requests from anyone, but that does not mean it will be merged. + +## What kind of change does this PR introduce? +* Fix +* Feature +* Codestyle +* Refactor +* Other + +## Did this PR introduce a breaking change? +_A breaking change includes anything that breaks backwards compatibility either at compile or run time._ +* Yes, please list breaking changes +* No diff --git a/.github/dependabot.yml b/.github/dependabot.yml index edeed94d..dcc3a543 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,12 +1,12 @@ -version: 2 -updates: - - package-ecosystem: "gradle" # See documentation for possible values - directory: "/" # Location of package manifests - target-branch: "dev" - schedule: - interval: "daily" - - package-ecosystem: "github-actions" - directory: "/" # Location of package manifests - target-branch: "dev" - schedule: - interval: "weekly" +version: 2 +updates: + - package-ecosystem: "gradle" # See documentation for possible values + directory: "/" # Location of package manifests + target-branch: "dev" + schedule: + interval: "daily" + - package-ecosystem: "github-actions" + directory: "/" # Location of package manifests + target-branch: "dev" + schedule: + interval: "weekly" diff --git a/.github/workflows/build_ci.yml b/.github/workflows/build_ci.yml index bdd8e1ce..1e0ef849 100644 --- a/.github/workflows/build_ci.yml +++ b/.github/workflows/build_ci.yml @@ -1,49 +1,49 @@ -name: Build and test with Gradle - -on: [push, pull_request] - -jobs: - test_linux: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 8 - uses: actions/setup-java@v2.1.0 - with: - distribution: adopt - java-version: 8 - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - name: Build and test with Gradle - run: ./gradlew test - - test_windows: - runs-on: windows-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 8 - uses: actions/setup-java@v2.1.0 - with: - distribution: adopt - java-version: 8 - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - name: Build and test with Gradle - run: ./gradlew test - - test_mac: - runs-on: macos-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 8 - uses: actions/setup-java@v2.1.0 - with: - distribution: adopt - java-version: 8 - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - name: Build and test with Gradle - run: ./gradlew test +name: Build and test with Gradle + +on: [push, pull_request] + +jobs: + test_linux: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 8 + uses: actions/setup-java@v2.1.0 + with: + distribution: adopt + java-version: 8 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build and test with Gradle + run: ./gradlew test + + test_windows: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 8 + uses: actions/setup-java@v2.1.0 + with: + distribution: adopt + java-version: 8 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build and test with Gradle + run: ./gradlew test + + test_mac: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 8 + uses: actions/setup-java@v2.1.0 + with: + distribution: adopt + java-version: 8 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build and test with Gradle + run: ./gradlew test diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index fc2861c5..9cf31280 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -1,28 +1,28 @@ -name: "Validate Gradle Wrapper" - -on: - push: - branches: - - master - - dev - pull_request: - branches: - - master - - dev - -jobs: - validationPlugin: - name: "Validation Plugin" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: gradle/wrapper-validation-action@v1 - validationJavaSample: - name: "Validation Java Sample" - runs-on: ubuntu-latest - defaults: - run: - working-directory: demo/java - steps: - - uses: actions/checkout@v2 - - uses: gradle/wrapper-validation-action@v1 +name: "Validate Gradle Wrapper" + +on: + push: + branches: + - master + - dev + pull_request: + branches: + - master + - dev + +jobs: + validationPlugin: + name: "Validation Plugin" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: gradle/wrapper-validation-action@v1 + validationJavaSample: + name: "Validation Java Sample" + runs-on: ubuntu-latest + defaults: + run: + working-directory: demo/java + steps: + - uses: actions/checkout@v2 + - uses: gradle/wrapper-validation-action@v1 diff --git a/.github/workflows/release_ci.yml b/.github/workflows/release_ci.yml index 7f514dbe..3244c4f1 100644 --- a/.github/workflows/release_ci.yml +++ b/.github/workflows/release_ci.yml @@ -1,47 +1,47 @@ -name: Create GitHub release(s) - -on: - push: - branches: [ master, dev ] - tags: 'v*' - -jobs: - build-and-release: - if: ${{ startsWith(github.ref, 'refs/tags/v') || github.ref != 'ref/heads/master' }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 8 - uses: actions/setup-java@v2.1.0 - with: - distribution: adopt - java-version: 8 - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - - name: Build release shadow jar with Gradle - run: ./gradlew -Penv=release shadowJar -x test - if: ${{ startsWith(github.ref, 'refs/tags/v') }} - - - name: Build dev shadow jar with Gradle - run: | - SHA_SHORT="$(git rev-parse --short HEAD)" - ./gradlew -Phash=$SHA_SHORT shadowJar -x test - if: ${{ !startsWith(github.ref, 'refs/tags/v') && github.ref != 'refs/heads/master' }} - - - uses: eine/tip@master - with: - token: ${{ secrets.GITHUB_TOKEN }} - tag: 'Dev' - rm: true - files: | - EOCV-Sim/build/libs/*.jar - if: ${{ github.event_name == 'push' && github.ref != 'refs/heads/master' && !startsWith(github.ref, 'refs/tags/v')}} - - - uses: softprops/action-gh-release@v1 - if: ${{ startsWith(github.ref, 'refs/tags/v') }} - with: - files: 'EOCV-Sim/build/libs/*.jar' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +name: Create GitHub release(s) + +on: + push: + branches: [ master, dev ] + tags: 'v*' + +jobs: + build-and-release: + if: ${{ startsWith(github.ref, 'refs/tags/v') || github.ref != 'ref/heads/master' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 8 + uses: actions/setup-java@v2.1.0 + with: + distribution: adopt + java-version: 8 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build release shadow jar with Gradle + run: ./gradlew -Penv=release shadowJar -x test + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + + - name: Build dev shadow jar with Gradle + run: | + SHA_SHORT="$(git rev-parse --short HEAD)" + ./gradlew -Phash=$SHA_SHORT shadowJar -x test + if: ${{ !startsWith(github.ref, 'refs/tags/v') && github.ref != 'refs/heads/master' }} + + - uses: eine/tip@master + with: + token: ${{ secrets.GITHUB_TOKEN }} + tag: 'Dev' + rm: true + files: | + EOCV-Sim/build/libs/*.jar + if: ${{ github.event_name == 'push' && github.ref != 'refs/heads/master' && !startsWith(github.ref, 'refs/tags/v')}} + + - uses: softprops/action-gh-release@v1 + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + with: + files: 'EOCV-Sim/build/libs/*.jar' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/update-gradle-wrapper.yml b/.github/workflows/update-gradle-wrapper.yml index 4608393c..528180c5 100644 --- a/.github/workflows/update-gradle-wrapper.yml +++ b/.github/workflows/update-gradle-wrapper.yml @@ -1,18 +1,18 @@ -name: Update Gradle Wrapper - -on: - schedule: - - cron: "0 6 * * MON" - -jobs: - update-gradle-wrapper: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2.3.4 - - - name: Update Gradle Wrapper - uses: gradle-update/update-gradle-wrapper-action@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - set-distribution-checksum: false +name: Update Gradle Wrapper + +on: + schedule: + - cron: "0 6 * * MON" + +jobs: + update-gradle-wrapper: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2.3.4 + + - name: Update Gradle Wrapper + uses: gradle-update/update-gradle-wrapper-action@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + set-distribution-checksum: false diff --git a/.gitignore b/.gitignore index 9ba30bc2..b1d499f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,113 +1,113 @@ -# Created by https://www.toptal.com/developers/gitignore/api/intellij -# Edit at https://www.toptal.com/developers/gitignore?templates=intellij - -### Intellij ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -.idea/ -.gradle/ - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -.idea/artifacts -.idea/compiler.xml -.idea/jarRepositories.xml -.idea/modules.xml -.idea/*.iml -.idea/modules -*.iml -*.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -### Intellij Patch ### -# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 - -# *.iml -# modules.xml -# .idea/misc.xml -# *.ipr - -# Sonarlint plugin -# https://plugins.jetbrains.com/plugin/7973-sonarlint -.idea/**/sonarlint/ - -# SonarQube Plugin -# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin -.idea/**/sonarIssues.xml - -# Markdown Navigator plugin -# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced -.idea/**/markdown-navigator.xml -.idea/**/markdown-navigator-enh.xml -.idea/**/markdown-navigator/ - -# Cache file creation bug -# See https://youtrack.jetbrains.com/issue/JBR-2257 -.idea/$CACHE_FILE$ - -# CodeStream plugin -# https://plugins.jetbrains.com/plugin/12206-codestream -.idea/codestream.xml - -# End of https://www.toptal.com/developers/gitignore/api/intellij - -!/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SkystoneDeterminationPipeline.java -!/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/StageSwitchingPipeline.java -!/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/StoneOrientationAnalysisPipeline.java -!/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SimpleThresholdPipeline.java - -*.log - -**/Build.java -**/build/* - -*.DS_Store - +# Created by https://www.toptal.com/developers/gitignore/api/intellij +# Edit at https://www.toptal.com/developers/gitignore?templates=intellij + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +.idea/ +.gradle/ + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +.idea/artifacts +.idea/compiler.xml +.idea/jarRepositories.xml +.idea/modules.xml +.idea/*.iml +.idea/modules +*.iml +*.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# End of https://www.toptal.com/developers/gitignore/api/intellij + +!/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SkystoneDeterminationPipeline.java +!/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/StageSwitchingPipeline.java +!/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/StoneOrientationAnalysisPipeline.java +!/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SimpleThresholdPipeline.java + +*.log + +**/Build.java +**/build/* + +*.DS_Store + **/imgui.ini \ No newline at end of file diff --git a/.replit b/.replit index ab3c8d80..49d4f9bb 100644 --- a/.replit +++ b/.replit @@ -1,2 +1,2 @@ -language = "java10" +language = "java10" run = "chmod +x gradlew && ./gradlew runSim" \ No newline at end of file diff --git a/.run/Run Simulator.run.xml b/.run/Run Simulator.run.xml index 27a12fbb..df0d196d 100644 --- a/.run/Run Simulator.run.xml +++ b/.run/Run Simulator.run.xml @@ -1,23 +1,23 @@ - - - - - - - true - true - false - - + + + + + + + true + true + false + + \ No newline at end of file diff --git a/EOCV-Sim/build.gradle b/EOCV-Sim/build.gradle index b79d0e23..2a8bc68f 100644 --- a/EOCV-Sim/build.gradle +++ b/EOCV-Sim/build.gradle @@ -1,86 +1,86 @@ -import java.nio.file.Paths -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter - -plugins { - id 'java' - id 'org.jetbrains.kotlin.jvm' - id 'com.github.johnrengelman.shadow' version '6.1.0' - id 'maven-publish' -} - -apply from: '../build.common.gradle' - -task sourcesJar(type: Jar) { - from sourceSets.main.allJava - archiveClassifier = "sources" -} - -publishing { - publications { - mavenJava(MavenPublication) { - from components.java - artifact sourcesJar - } - } -} - -ext.kotest_version = '4.4.3' - -test { - useJUnitPlatform() -} - -apply from: '../test-logging.gradle' - -dependencies { - implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' - - implementation 'org.openpnp:opencv:4.3.0-2' - implementation 'com.github.sarxos:webcam-capture:0.3.12' - - implementation 'info.picocli:picocli:4.6.1' - implementation 'com.google.code.gson:gson:2.8.7' - implementation 'io.github.classgraph:classgraph:4.8.108' - - implementation 'com.formdev:flatlaf:1.2' - implementation 'com.formdev:flatlaf-intellij-themes:1.2' - - implementation 'net.lingala.zip4j:zip4j:2.8.0' - - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_coroutines_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-swing:$kotlinx_coroutines_version" - - testImplementation "io.kotest:kotest-runner-junit5:$kotest_version" - testImplementation "io.kotest:kotest-assertions-core:$kotest_version" -} - -task(writeBuildClassJava) { - - String date = DateTimeFormatter.ofPattern("yyyy-M-d hh:mm:ss").format(LocalDateTime.now()) - - File versionFile = Paths.get( - projectDir.absolutePath, 'src', 'main', 'java', - 'com', 'github', 'serivesmejia', 'eocvsim', 'Build.java' - ).toFile() - - versionFile.delete() - - versionFile << "package com.github.serivesmejia.eocvsim;\n" + - "\n" + - "/*\n" + - " * Autogenerated file! Do not manually edit this file. This file\n" + - " * is regenerated any time the build task is run.\n" + - " *\n" + - " * Based from PhotonVision PhotonVersion generator task\n"+ - " */\n" + - "@SuppressWarnings(\"ALL\")\n" + - "public final class Build {\n" + - " public static final String versionString = \"$version\";\n" + - " public static final String standardVersionString = \"$standardVersion\";\n" + - " public static final String buildDate = \"$date\";\n" + - " public static final boolean isDev = ${version.contains("dev")};\n" + - "}" -} - +import java.nio.file.Paths +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +plugins { + id 'java' + id 'org.jetbrains.kotlin.jvm' + id 'com.github.johnrengelman.shadow' version '6.1.0' + id 'maven-publish' +} + +apply from: '../build.common.gradle' + +task sourcesJar(type: Jar) { + from sourceSets.main.allJava + archiveClassifier = "sources" +} + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + artifact sourcesJar + } + } +} + +ext.kotest_version = '4.4.3' + +test { + useJUnitPlatform() +} + +apply from: '../test-logging.gradle' + +dependencies { + implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + + implementation 'org.openpnp:opencv:4.3.0-2' + implementation 'com.github.sarxos:webcam-capture:0.3.12' + + implementation 'info.picocli:picocli:4.6.1' + implementation 'com.google.code.gson:gson:2.8.7' + implementation 'io.github.classgraph:classgraph:4.8.108' + + implementation 'com.formdev:flatlaf:1.2' + implementation 'com.formdev:flatlaf-intellij-themes:1.2' + + implementation 'net.lingala.zip4j:zip4j:2.8.0' + + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_coroutines_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-swing:$kotlinx_coroutines_version" + + testImplementation "io.kotest:kotest-runner-junit5:$kotest_version" + testImplementation "io.kotest:kotest-assertions-core:$kotest_version" +} + +task(writeBuildClassJava) { + + String date = DateTimeFormatter.ofPattern("yyyy-M-d hh:mm:ss").format(LocalDateTime.now()) + + File versionFile = Paths.get( + projectDir.absolutePath, 'src', 'main', 'java', + 'com', 'github', 'serivesmejia', 'eocvsim', 'Build.java' + ).toFile() + + versionFile.delete() + + versionFile << "package com.github.serivesmejia.eocvsim;\n" + + "\n" + + "/*\n" + + " * Autogenerated file! Do not manually edit this file. This file\n" + + " * is regenerated any time the build task is run.\n" + + " *\n" + + " * Based from PhotonVision PhotonVersion generator task\n"+ + " */\n" + + "@SuppressWarnings(\"ALL\")\n" + + "public final class Build {\n" + + " public static final String versionString = \"$version\";\n" + + " public static final String standardVersionString = \"$standardVersion\";\n" + + " public static final String buildDate = \"$date\";\n" + + " public static final boolean isDev = ${version.contains("dev")};\n" + + "}" +} + build.dependsOn writeBuildClassJava \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt index 56e1fec6..8313e85f 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt @@ -1,384 +1,384 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim - -import com.github.serivesmejia.eocvsim.config.Config -import com.github.serivesmejia.eocvsim.config.ConfigManager -import com.github.serivesmejia.eocvsim.gui.DialogFactory -import com.github.serivesmejia.eocvsim.gui.Visualizer -import com.github.serivesmejia.eocvsim.gui.dialog.FileAlreadyExists -import com.github.serivesmejia.eocvsim.input.InputSourceManager -import com.github.serivesmejia.eocvsim.output.VideoRecordingSession -import com.github.serivesmejia.eocvsim.pipeline.PipelineManager -import com.github.serivesmejia.eocvsim.pipeline.PipelineSource -import com.github.serivesmejia.eocvsim.tuner.TunerManager -import com.github.serivesmejia.eocvsim.util.FileFilters -import com.github.serivesmejia.eocvsim.util.Log -import com.github.serivesmejia.eocvsim.util.SysUtil -import com.github.serivesmejia.eocvsim.util.event.EventHandler -import com.github.serivesmejia.eocvsim.util.exception.MaxActiveContextsException -import com.github.serivesmejia.eocvsim.util.exception.handling.EOCVSimUncaughtExceptionHandler -import com.github.serivesmejia.eocvsim.util.extension.plus -import com.github.serivesmejia.eocvsim.util.fps.FpsLimiter -import com.github.serivesmejia.eocvsim.util.io.EOCVSimFolder -import com.github.serivesmejia.eocvsim.workspace.WorkspaceManager -import nu.pattern.OpenCV -import org.opencv.core.Size -import java.awt.Dimension -import java.io.File -import javax.swing.SwingUtilities -import javax.swing.filechooser.FileFilter -import javax.swing.filechooser.FileNameExtensionFilter -import kotlin.concurrent.thread -import kotlin.system.exitProcess - -class EOCVSim(val params: Parameters = Parameters()) { - - companion object { - const val VERSION = Build.versionString - const val DEFAULT_EOCV_WIDTH = 320 - const val DEFAULT_EOCV_HEIGHT = 240 - @JvmField val DEFAULT_EOCV_SIZE = Size(DEFAULT_EOCV_WIDTH.toDouble(), DEFAULT_EOCV_HEIGHT.toDouble()) - - private const val TAG = "EOCVSim" - - private var isNativeLibLoaded = false - - fun loadOpenCvLib() { - if (isNativeLibLoaded) return - - Log.info(TAG, "Loading native lib...") - - try { - OpenCV.loadLocally() - Log.info(TAG, "Successfully loaded native lib") - } catch (ex: Throwable) { - Log.error(TAG, "Failure loading native lib", ex) - Log.info(TAG, "Retrying with old method...") - - if (!SysUtil.loadCvNativeLib()) exitProcess(-1) - } - - isNativeLibLoaded = true - } - } - - @JvmField - val onMainUpdate = EventHandler("OnMainUpdate") - - @JvmField - val visualizer = Visualizer(this) - - @JvmField - val configManager = ConfigManager() - @JvmField - val inputSourceManager = InputSourceManager(this) - @JvmField - val pipelineManager = PipelineManager(this) - @JvmField - val tunerManager = TunerManager(this) - @JvmField - val workspaceManager = WorkspaceManager(this) - - val config: Config - get() = configManager.config - - var currentRecordingSession: VideoRecordingSession? = null - val fpsLimiter = FpsLimiter(30.0) - - lateinit var eocvSimThread: Thread - private set - - private val hexCode = Integer.toHexString(hashCode()) - - enum class DestroyReason { - USER_REQUESTED, RESTART, CRASH - } - - fun init() { - eocvSimThread = Thread.currentThread() - - if(!EOCVSimFolder.couldLock) { - Log.error(TAG, - "Couldn't finally claim lock file in \"${EOCVSimFolder.absolutePath}\"! " + - "Is the folder opened by another EOCV-Sim instance?" - ) - - Log.error(TAG, "Unable to continue with the execution, the sim will exit now.") - exitProcess(-1) - } else { - Log.info(TAG, "Confirmed claiming of the lock file in ${EOCVSimFolder.absolutePath}") - Log.blank() - } - - DialogFactory.createSplashScreen(visualizer.onInitFinished) - - Log.info(TAG, "Initializing EasyOpenCV Simulator v$VERSION ($hexCode)") - Log.blank() - - EOCVSimUncaughtExceptionHandler.register() - - //loading native lib only once in the app runtime - loadOpenCvLib() - Log.blank() - - configManager.init() //load config - - workspaceManager.init() - - visualizer.initAsync(configManager.config.simTheme) //create gui in the EDT - - inputSourceManager.init() //loading user created input sources - pipelineManager.init() //init pipeline manager (scan for pipelines) - tunerManager.init() //init tunable variables manager - - //shows a warning when a pipeline gets "stuck" - pipelineManager.onPipelineTimeout { - visualizer.asyncPleaseWaitDialog( - "Current pipeline took too long to ${pipelineManager.lastPipelineAction}", - "Falling back to DefaultPipeline", - "Close", Dimension(310, 150), true, true - ) - } - - inputSourceManager.inputSourceLoader.saveInputSourcesToFile() - - visualizer.waitForFinishingInit() - - visualizer.sourceSelectorPanel.updateSourcesList() //update sources and pick first one - visualizer.sourceSelectorPanel.sourceSelector.selectedIndex = 0 - visualizer.sourceSelectorPanel.allowSourceSwitching = true - - visualizer.pipelineSelectorPanel.updatePipelinesList() //update pipelines and pick first one (DefaultPipeline) - visualizer.pipelineSelectorPanel.selectedIndex = 0 - - //post output mats from the pipeline to the visualizer viewport - pipelineManager.pipelineOutputPosters.add(visualizer.viewport.matPoster) - - start() - } - - private fun start() { - Log.info(TAG, "Begin EOCVSim loop") - Log.blank() - - while (!eocvSimThread.isInterrupted) { - //run all pending requested runnables - onMainUpdate.run() - - updateVisualizerTitle() - - inputSourceManager.update(pipelineManager.paused) - tunerManager.update() - - try { - pipelineManager.update( - if(inputSourceManager.lastMatFromSource != null && !inputSourceManager.lastMatFromSource.empty()) { - inputSourceManager.lastMatFromSource - } else null - ) - } catch (ex: MaxActiveContextsException) { //handles when a lot of pipelines are stuck in the background - visualizer.asyncPleaseWaitDialog( - "There are many pipelines stuck in processFrame running in the background", - "To avoid further issues, EOCV-Sim will exit now.", - "Ok", - Dimension(450, 150), - true, true - ).onCancel { - destroy(DestroyReason.CRASH) //destroy eocv sim when pressing "exit" - } - - //print exception - Log.error( - TAG, - "Please note that the following exception is likely to be caused by one or more of the user pipelines", - ex - ) - - //block the current thread until the user closes the dialog - try { - //using sleep for avoiding burning cpu cycles - Thread.sleep(Long.MAX_VALUE) - } catch (ignored: InterruptedException) { - //reinterrupt once user closes the dialog - Thread.currentThread().interrupt() - } - - break //bye bye - } - - //updating displayed telemetry - visualizer.telemetryPanel.updateTelemetry(pipelineManager.currentTelemetry) - - //limit FPG - fpsLimiter.maxFPS = config.pipelineMaxFps.fps.toDouble() - fpsLimiter.sync() - } - - Log.warn(TAG, "Main thread interrupted (" + Integer.toHexString(hashCode()) + ")") - } - - fun destroy(reason: DestroyReason) { - Log.warn(TAG, "Destroying current EOCVSim ($hexCode) due to $reason, it is normal to see InterruptedExceptions and other kinds of stack traces below") - - //stop recording session if there's currently an ongoing one - currentRecordingSession?.stopRecordingSession() - currentRecordingSession?.discardVideo() - - Log.info(TAG, "Trying to save config file...") - - inputSourceManager.currentInputSource?.close() - workspaceManager.stopFileWatcher() - configManager.saveToFile() - visualizer.close() - - eocvSimThread.interrupt() - - if(reason == DestroyReason.USER_REQUESTED || reason == DestroyReason.CRASH) - jvmMainThread.interrupt() - } - - fun destroy() { - destroy(DestroyReason.USER_REQUESTED) - } - - fun restart() { - Log.info(TAG, "Restarting...") - - pipelineManager.captureStaticSnapshot() - - Log.blank() - destroy(DestroyReason.RESTART) - Log.blank() - - currentMainThread = Thread( - { EOCVSim(params).init() }, - "new-main" - ) - currentMainThread.start() //run next instance on a new main thread for the old one to get interrupted and ended - - if(Thread.currentThread() == jvmMainThread) { - Thread.interrupted() // clear interrupt state - Thread.sleep(Long.MAX_VALUE) // hang forever the jvm main thread so that the app doesnt die idk - } - } - - fun startRecordingSession() { - if (currentRecordingSession == null) { - currentRecordingSession = VideoRecordingSession( - config.videoRecordingFps.fps.toDouble(), config.videoRecordingSize - ) - - currentRecordingSession!!.startRecordingSession() - - Log.info(TAG, "Recording session started") - - pipelineManager.pipelineOutputPosters.add(currentRecordingSession!!.matPoster) - } - } - - //stopping recording session and saving file - fun stopRecordingSession() { - currentRecordingSession?.let { itVideo -> - - visualizer.pipelineSelectorPanel.buttonsPanel.pipelineRecordBtt.isEnabled = false - - itVideo.stopRecordingSession() - pipelineManager.pipelineOutputPosters.remove(itVideo.matPoster) - - Log.info(TAG, "Recording session stopped") - - DialogFactory.createFileChooser( - visualizer.frame, - DialogFactory.FileChooser.Mode.SAVE_FILE_SELECT, FileFilters.recordedVideoFilter - ).addCloseListener { _: Int, file: File?, selectedFileFilter: FileFilter? -> - onMainUpdate.doOnce { - if (file != null) { - - var correctedFile = File(file.absolutePath) - val extension = SysUtil.getExtensionByStringHandling(file.name) - - if (selectedFileFilter is FileNameExtensionFilter) { //if user selected an extension - //get selected extension - correctedFile = file + "." + selectedFileFilter.extensions[0] - } else if (extension.isPresent) { - if (!extension.get().equals("avi", true)) { - correctedFile = file + ".avi" - } - } else { - correctedFile = file + ".avi" - } - - if (correctedFile.exists()) { - SwingUtilities.invokeLater { - if (DialogFactory.createFileAlreadyExistsDialog(this) == FileAlreadyExists.UserChoice.REPLACE) { - onMainUpdate.doOnce { itVideo.saveTo(correctedFile) } - } - } - } else { - itVideo.saveTo(correctedFile) - } - } else { - itVideo.discardVideo() - } - - currentRecordingSession = null - visualizer.pipelineSelectorPanel.buttonsPanel.pipelineRecordBtt.isEnabled = true - } - } - } - } - - fun isCurrentlyRecording() = currentRecordingSession?.isRecording ?: false - - private fun updateVisualizerTitle() { - val isBuildRunning = if (pipelineManager.compiledPipelineManager.isBuildRunning) "(Building)" else "" - - val workspaceMsg = " - ${workspaceManager.workspaceFile.absolutePath} $isBuildRunning" - - val pipelineFpsMsg = " (${pipelineManager.pipelineFpsCounter.fps} Pipeline FPS)" - val posterFpsMsg = " (${visualizer.viewport.matPoster.fpsCounter.fps} Poster FPS)" - val isPaused = if (pipelineManager.paused) " (Paused)" else "" - val isRecording = if (isCurrentlyRecording()) " RECORDING" else "" - - val msg = isRecording + pipelineFpsMsg + posterFpsMsg + isPaused - - if (pipelineManager.currentPipeline == null) { - visualizer.setTitleMessage("No pipeline$msg${workspaceMsg}") - } else { - visualizer.setTitleMessage("${pipelineManager.currentPipelineName}$msg${workspaceMsg}") - } - } - - class Parameters { - var scanForPipelinesIn = "org.firstinspires" - var scanForTunableFieldsIn = "com.github.serivesmejia" - - var initialWorkspace: File? = null - - var initialPipelineName: String? = null - var initialPipelineSource: PipelineSource? = null - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim + +import com.github.serivesmejia.eocvsim.config.Config +import com.github.serivesmejia.eocvsim.config.ConfigManager +import com.github.serivesmejia.eocvsim.gui.DialogFactory +import com.github.serivesmejia.eocvsim.gui.Visualizer +import com.github.serivesmejia.eocvsim.gui.dialog.FileAlreadyExists +import com.github.serivesmejia.eocvsim.input.InputSourceManager +import com.github.serivesmejia.eocvsim.output.VideoRecordingSession +import com.github.serivesmejia.eocvsim.pipeline.PipelineManager +import com.github.serivesmejia.eocvsim.pipeline.PipelineSource +import com.github.serivesmejia.eocvsim.tuner.TunerManager +import com.github.serivesmejia.eocvsim.util.FileFilters +import com.github.serivesmejia.eocvsim.util.Log +import com.github.serivesmejia.eocvsim.util.SysUtil +import com.github.serivesmejia.eocvsim.util.event.EventHandler +import com.github.serivesmejia.eocvsim.util.exception.MaxActiveContextsException +import com.github.serivesmejia.eocvsim.util.exception.handling.EOCVSimUncaughtExceptionHandler +import com.github.serivesmejia.eocvsim.util.extension.plus +import com.github.serivesmejia.eocvsim.util.fps.FpsLimiter +import com.github.serivesmejia.eocvsim.util.io.EOCVSimFolder +import com.github.serivesmejia.eocvsim.workspace.WorkspaceManager +import nu.pattern.OpenCV +import org.opencv.core.Size +import java.awt.Dimension +import java.io.File +import javax.swing.SwingUtilities +import javax.swing.filechooser.FileFilter +import javax.swing.filechooser.FileNameExtensionFilter +import kotlin.concurrent.thread +import kotlin.system.exitProcess + +class EOCVSim(val params: Parameters = Parameters()) { + + companion object { + const val VERSION = Build.versionString + const val DEFAULT_EOCV_WIDTH = 320 + const val DEFAULT_EOCV_HEIGHT = 240 + @JvmField val DEFAULT_EOCV_SIZE = Size(DEFAULT_EOCV_WIDTH.toDouble(), DEFAULT_EOCV_HEIGHT.toDouble()) + + private const val TAG = "EOCVSim" + + private var isNativeLibLoaded = false + + fun loadOpenCvLib() { + if (isNativeLibLoaded) return + + Log.info(TAG, "Loading native lib...") + + try { + OpenCV.loadLocally() + Log.info(TAG, "Successfully loaded native lib") + } catch (ex: Throwable) { + Log.error(TAG, "Failure loading native lib", ex) + Log.info(TAG, "Retrying with old method...") + + if (!SysUtil.loadCvNativeLib()) exitProcess(-1) + } + + isNativeLibLoaded = true + } + } + + @JvmField + val onMainUpdate = EventHandler("OnMainUpdate") + + @JvmField + val visualizer = Visualizer(this) + + @JvmField + val configManager = ConfigManager() + @JvmField + val inputSourceManager = InputSourceManager(this) + @JvmField + val pipelineManager = PipelineManager(this) + @JvmField + val tunerManager = TunerManager(this) + @JvmField + val workspaceManager = WorkspaceManager(this) + + val config: Config + get() = configManager.config + + var currentRecordingSession: VideoRecordingSession? = null + val fpsLimiter = FpsLimiter(30.0) + + lateinit var eocvSimThread: Thread + private set + + private val hexCode = Integer.toHexString(hashCode()) + + enum class DestroyReason { + USER_REQUESTED, RESTART, CRASH + } + + fun init() { + eocvSimThread = Thread.currentThread() + + if(!EOCVSimFolder.couldLock) { + Log.error(TAG, + "Couldn't finally claim lock file in \"${EOCVSimFolder.absolutePath}\"! " + + "Is the folder opened by another EOCV-Sim instance?" + ) + + Log.error(TAG, "Unable to continue with the execution, the sim will exit now.") + exitProcess(-1) + } else { + Log.info(TAG, "Confirmed claiming of the lock file in ${EOCVSimFolder.absolutePath}") + Log.blank() + } + + DialogFactory.createSplashScreen(visualizer.onInitFinished) + + Log.info(TAG, "Initializing EasyOpenCV Simulator v$VERSION ($hexCode)") + Log.blank() + + EOCVSimUncaughtExceptionHandler.register() + + //loading native lib only once in the app runtime + loadOpenCvLib() + Log.blank() + + configManager.init() //load config + + workspaceManager.init() + + visualizer.initAsync(configManager.config.simTheme) //create gui in the EDT + + inputSourceManager.init() //loading user created input sources + pipelineManager.init() //init pipeline manager (scan for pipelines) + tunerManager.init() //init tunable variables manager + + //shows a warning when a pipeline gets "stuck" + pipelineManager.onPipelineTimeout { + visualizer.asyncPleaseWaitDialog( + "Current pipeline took too long to ${pipelineManager.lastPipelineAction}", + "Falling back to DefaultPipeline", + "Close", Dimension(310, 150), true, true + ) + } + + inputSourceManager.inputSourceLoader.saveInputSourcesToFile() + + visualizer.waitForFinishingInit() + + visualizer.sourceSelectorPanel.updateSourcesList() //update sources and pick first one + visualizer.sourceSelectorPanel.sourceSelector.selectedIndex = 0 + visualizer.sourceSelectorPanel.allowSourceSwitching = true + + visualizer.pipelineSelectorPanel.updatePipelinesList() //update pipelines and pick first one (DefaultPipeline) + visualizer.pipelineSelectorPanel.selectedIndex = 0 + + //post output mats from the pipeline to the visualizer viewport + pipelineManager.pipelineOutputPosters.add(visualizer.viewport.matPoster) + + start() + } + + private fun start() { + Log.info(TAG, "Begin EOCVSim loop") + Log.blank() + + while (!eocvSimThread.isInterrupted) { + //run all pending requested runnables + onMainUpdate.run() + + updateVisualizerTitle() + + inputSourceManager.update(pipelineManager.paused) + tunerManager.update() + + try { + pipelineManager.update( + if(inputSourceManager.lastMatFromSource != null && !inputSourceManager.lastMatFromSource.empty()) { + inputSourceManager.lastMatFromSource + } else null + ) + } catch (ex: MaxActiveContextsException) { //handles when a lot of pipelines are stuck in the background + visualizer.asyncPleaseWaitDialog( + "There are many pipelines stuck in processFrame running in the background", + "To avoid further issues, EOCV-Sim will exit now.", + "Ok", + Dimension(450, 150), + true, true + ).onCancel { + destroy(DestroyReason.CRASH) //destroy eocv sim when pressing "exit" + } + + //print exception + Log.error( + TAG, + "Please note that the following exception is likely to be caused by one or more of the user pipelines", + ex + ) + + //block the current thread until the user closes the dialog + try { + //using sleep for avoiding burning cpu cycles + Thread.sleep(Long.MAX_VALUE) + } catch (ignored: InterruptedException) { + //reinterrupt once user closes the dialog + Thread.currentThread().interrupt() + } + + break //bye bye + } + + //updating displayed telemetry + visualizer.telemetryPanel.updateTelemetry(pipelineManager.currentTelemetry) + + //limit FPG + fpsLimiter.maxFPS = config.pipelineMaxFps.fps.toDouble() + fpsLimiter.sync() + } + + Log.warn(TAG, "Main thread interrupted (" + Integer.toHexString(hashCode()) + ")") + } + + fun destroy(reason: DestroyReason) { + Log.warn(TAG, "Destroying current EOCVSim ($hexCode) due to $reason, it is normal to see InterruptedExceptions and other kinds of stack traces below") + + //stop recording session if there's currently an ongoing one + currentRecordingSession?.stopRecordingSession() + currentRecordingSession?.discardVideo() + + Log.info(TAG, "Trying to save config file...") + + inputSourceManager.currentInputSource?.close() + workspaceManager.stopFileWatcher() + configManager.saveToFile() + visualizer.close() + + eocvSimThread.interrupt() + + if(reason == DestroyReason.USER_REQUESTED || reason == DestroyReason.CRASH) + jvmMainThread.interrupt() + } + + fun destroy() { + destroy(DestroyReason.USER_REQUESTED) + } + + fun restart() { + Log.info(TAG, "Restarting...") + + pipelineManager.captureStaticSnapshot() + + Log.blank() + destroy(DestroyReason.RESTART) + Log.blank() + + currentMainThread = Thread( + { EOCVSim(params).init() }, + "new-main" + ) + currentMainThread.start() //run next instance on a new main thread for the old one to get interrupted and ended + + if(Thread.currentThread() == jvmMainThread) { + Thread.interrupted() // clear interrupt state + Thread.sleep(Long.MAX_VALUE) // hang forever the jvm main thread so that the app doesnt die idk + } + } + + fun startRecordingSession() { + if (currentRecordingSession == null) { + currentRecordingSession = VideoRecordingSession( + config.videoRecordingFps.fps.toDouble(), config.videoRecordingSize + ) + + currentRecordingSession!!.startRecordingSession() + + Log.info(TAG, "Recording session started") + + pipelineManager.pipelineOutputPosters.add(currentRecordingSession!!.matPoster) + } + } + + //stopping recording session and saving file + fun stopRecordingSession() { + currentRecordingSession?.let { itVideo -> + + visualizer.pipelineSelectorPanel.buttonsPanel.pipelineRecordBtt.isEnabled = false + + itVideo.stopRecordingSession() + pipelineManager.pipelineOutputPosters.remove(itVideo.matPoster) + + Log.info(TAG, "Recording session stopped") + + DialogFactory.createFileChooser( + visualizer.frame, + DialogFactory.FileChooser.Mode.SAVE_FILE_SELECT, FileFilters.recordedVideoFilter + ).addCloseListener { _: Int, file: File?, selectedFileFilter: FileFilter? -> + onMainUpdate.doOnce { + if (file != null) { + + var correctedFile = File(file.absolutePath) + val extension = SysUtil.getExtensionByStringHandling(file.name) + + if (selectedFileFilter is FileNameExtensionFilter) { //if user selected an extension + //get selected extension + correctedFile = file + "." + selectedFileFilter.extensions[0] + } else if (extension.isPresent) { + if (!extension.get().equals("avi", true)) { + correctedFile = file + ".avi" + } + } else { + correctedFile = file + ".avi" + } + + if (correctedFile.exists()) { + SwingUtilities.invokeLater { + if (DialogFactory.createFileAlreadyExistsDialog(this) == FileAlreadyExists.UserChoice.REPLACE) { + onMainUpdate.doOnce { itVideo.saveTo(correctedFile) } + } + } + } else { + itVideo.saveTo(correctedFile) + } + } else { + itVideo.discardVideo() + } + + currentRecordingSession = null + visualizer.pipelineSelectorPanel.buttonsPanel.pipelineRecordBtt.isEnabled = true + } + } + } + } + + fun isCurrentlyRecording() = currentRecordingSession?.isRecording ?: false + + private fun updateVisualizerTitle() { + val isBuildRunning = if (pipelineManager.compiledPipelineManager.isBuildRunning) "(Building)" else "" + + val workspaceMsg = " - ${workspaceManager.workspaceFile.absolutePath} $isBuildRunning" + + val pipelineFpsMsg = " (${pipelineManager.pipelineFpsCounter.fps} Pipeline FPS)" + val posterFpsMsg = " (${visualizer.viewport.matPoster.fpsCounter.fps} Poster FPS)" + val isPaused = if (pipelineManager.paused) " (Paused)" else "" + val isRecording = if (isCurrentlyRecording()) " RECORDING" else "" + + val msg = isRecording + pipelineFpsMsg + posterFpsMsg + isPaused + + if (pipelineManager.currentPipeline == null) { + visualizer.setTitleMessage("No pipeline$msg${workspaceMsg}") + } else { + visualizer.setTitleMessage("${pipelineManager.currentPipelineName}$msg${workspaceMsg}") + } + } + + class Parameters { + var scanForPipelinesIn = "org.firstinspires" + var scanForTunableFieldsIn = "com.github.serivesmejia" + + var initialWorkspace: File? = null + + var initialPipelineName: String? = null + var initialPipelineSource: PipelineSource? = null + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt index 1330354e..3547fc52 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt @@ -1,69 +1,69 @@ -@file:JvmName("Main") -package com.github.serivesmejia.eocvsim - -import com.github.serivesmejia.eocvsim.pipeline.PipelineSource -import com.github.serivesmejia.eocvsim.util.Log -import picocli.CommandLine -import java.io.File -import java.nio.file.Paths -import kotlin.system.exitProcess - -val jvmMainThread: Thread = Thread.currentThread() -var currentMainThread: Thread = jvmMainThread - -fun main(args: Array) { - val result = CommandLine( - EOCVSimCommandInterface() - ).setCaseInsensitiveEnumValuesAllowed(true).execute(*args) - - exitProcess(result) -} - -@CommandLine.Command(name = "eocvsim", mixinStandardHelpOptions = true, version = [Build.versionString]) -class EOCVSimCommandInterface : Runnable { - - @CommandLine.Option(names = ["-w", "--workspace"], description = ["Specifies the workspace that will be used only during this run, path can be relative or absolute"]) - @JvmField var workspacePath = "" - - @CommandLine.Option(names = ["-p", "--pipeline"], description = ["Specifies the pipeline selected when the simulator starts, and the initial runtime build finishes if it was running"]) - @JvmField var initialPipeline = "" - - @CommandLine.Option(names = ["-s", "--source"], description = ["Specifies the source of the pipeline that will be selected when the simulator starts, from the --pipeline argument. Defaults to CLASSPATH. Possible values: \${COMPLETION-CANDIDATES}"]) - @JvmField var initialPipelineSource = PipelineSource.CLASSPATH - - override fun run() { - val parameters = EOCVSim.Parameters() - - if(workspacePath.trim() != "") { - var file = File(workspacePath) - - if(!file.exists()) { - file = Paths.get(System.getProperty("user.dir"), workspacePath).toFile() - - if(!file.exists()) { - Log.error("Workspace path is not valid, folder doesn't exist (tried in \"$workspacePath\" and \"${file.absolutePath})\"") - exitProcess(1) - } - } - - if(!file.isDirectory) { - Log.error("Workspace path is not valid, the specified path is not a folder") - exitProcess(1) - } - - Log.info("Workspace from command line: ${file.absolutePath}") - - parameters.initialWorkspace = file - } - - if(initialPipeline.trim() != "") { - parameters.initialPipelineName = initialPipeline - parameters.initialPipelineSource = initialPipelineSource - - Log.info("Initial pipeline from command line: $initialPipeline coming from $initialPipelineSource") - } - - EOCVSim(parameters).init() - } - +@file:JvmName("Main") +package com.github.serivesmejia.eocvsim + +import com.github.serivesmejia.eocvsim.pipeline.PipelineSource +import com.github.serivesmejia.eocvsim.util.Log +import picocli.CommandLine +import java.io.File +import java.nio.file.Paths +import kotlin.system.exitProcess + +val jvmMainThread: Thread = Thread.currentThread() +var currentMainThread: Thread = jvmMainThread + +fun main(args: Array) { + val result = CommandLine( + EOCVSimCommandInterface() + ).setCaseInsensitiveEnumValuesAllowed(true).execute(*args) + + exitProcess(result) +} + +@CommandLine.Command(name = "eocvsim", mixinStandardHelpOptions = true, version = [Build.versionString]) +class EOCVSimCommandInterface : Runnable { + + @CommandLine.Option(names = ["-w", "--workspace"], description = ["Specifies the workspace that will be used only during this run, path can be relative or absolute"]) + @JvmField var workspacePath = "" + + @CommandLine.Option(names = ["-p", "--pipeline"], description = ["Specifies the pipeline selected when the simulator starts, and the initial runtime build finishes if it was running"]) + @JvmField var initialPipeline = "" + + @CommandLine.Option(names = ["-s", "--source"], description = ["Specifies the source of the pipeline that will be selected when the simulator starts, from the --pipeline argument. Defaults to CLASSPATH. Possible values: \${COMPLETION-CANDIDATES}"]) + @JvmField var initialPipelineSource = PipelineSource.CLASSPATH + + override fun run() { + val parameters = EOCVSim.Parameters() + + if(workspacePath.trim() != "") { + var file = File(workspacePath) + + if(!file.exists()) { + file = Paths.get(System.getProperty("user.dir"), workspacePath).toFile() + + if(!file.exists()) { + Log.error("Workspace path is not valid, folder doesn't exist (tried in \"$workspacePath\" and \"${file.absolutePath})\"") + exitProcess(1) + } + } + + if(!file.isDirectory) { + Log.error("Workspace path is not valid, the specified path is not a folder") + exitProcess(1) + } + + Log.info("Workspace from command line: ${file.absolutePath}") + + parameters.initialWorkspace = file + } + + if(initialPipeline.trim() != "") { + parameters.initialPipelineName = initialPipeline + parameters.initialPipelineSource = initialPipelineSource + + Log.info("Initial pipeline from command line: $initialPipeline coming from $initialPipelineSource") + } + + EOCVSim(parameters).init() + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/config/Config.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/config/Config.java index d70fbf7e..a2fb232f 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/config/Config.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/config/Config.java @@ -1,61 +1,61 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.config; - -import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; -import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanelConfig; -import com.github.serivesmejia.eocvsim.gui.theme.Theme; -import com.github.serivesmejia.eocvsim.pipeline.PipelineFps; -import com.github.serivesmejia.eocvsim.pipeline.PipelineTimeout; -import com.github.serivesmejia.eocvsim.pipeline.compiler.CompiledPipelineManager; -import org.opencv.core.Size; - -import java.util.HashMap; - -public class Config { - public volatile Theme simTheme = Theme.Light; - - public volatile double zoom = 1; - - public volatile PipelineFps pipelineMaxFps = PipelineFps.MEDIUM; - public volatile PipelineTimeout pipelineTimeout = PipelineTimeout.MEDIUM; - - public volatile boolean pauseOnImages = true; - - public volatile Size videoRecordingSize = new Size(640, 480); - public volatile PipelineFps videoRecordingFps = PipelineFps.MEDIUM; - - public volatile String workspacePath = CompiledPipelineManager.Companion.getDEF_WORKSPACE_FOLDER().getAbsolutePath(); - - public volatile TunableFieldPanelConfig.Config globalTunableFieldsConfig = - new TunableFieldPanelConfig.Config( - new Size(0, 255), - TunableFieldPanelConfig.PickerColorSpace.RGB, - TunableFieldPanel.Mode.TEXTBOXES, - TunableFieldPanelConfig.ConfigSource.GLOBAL_DEFAULT - ); - - public volatile HashMap specificTunableFieldConfig = new HashMap<>(); - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.config; + +import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; +import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanelConfig; +import com.github.serivesmejia.eocvsim.gui.theme.Theme; +import com.github.serivesmejia.eocvsim.pipeline.PipelineFps; +import com.github.serivesmejia.eocvsim.pipeline.PipelineTimeout; +import com.github.serivesmejia.eocvsim.pipeline.compiler.CompiledPipelineManager; +import org.opencv.core.Size; + +import java.util.HashMap; + +public class Config { + public volatile Theme simTheme = Theme.Light; + + public volatile double zoom = 1; + + public volatile PipelineFps pipelineMaxFps = PipelineFps.MEDIUM; + public volatile PipelineTimeout pipelineTimeout = PipelineTimeout.MEDIUM; + + public volatile boolean pauseOnImages = true; + + public volatile Size videoRecordingSize = new Size(640, 480); + public volatile PipelineFps videoRecordingFps = PipelineFps.MEDIUM; + + public volatile String workspacePath = CompiledPipelineManager.Companion.getDEF_WORKSPACE_FOLDER().getAbsolutePath(); + + public volatile TunableFieldPanelConfig.Config globalTunableFieldsConfig = + new TunableFieldPanelConfig.Config( + new Size(0, 255), + TunableFieldPanelConfig.PickerColorSpace.RGB, + TunableFieldPanel.Mode.TEXTBOXES, + TunableFieldPanelConfig.ConfigSource.GLOBAL_DEFAULT + ); + + public volatile HashMap specificTunableFieldConfig = new HashMap<>(); + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/config/ConfigLoader.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/config/ConfigLoader.java index b6a315cc..e392063d 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/config/ConfigLoader.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/config/ConfigLoader.java @@ -1,73 +1,73 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.config; - -import com.github.serivesmejia.eocvsim.util.Log; -import com.github.serivesmejia.eocvsim.util.SysUtil; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -import java.io.File; -import java.io.FileNotFoundException; - -public class ConfigLoader { - - public static final String CONFIG_SAVEFILE_NAME = "eocvsim_config.json"; - - public static final File CONFIG_SAVEFILE = new File(SysUtil.getEOCVSimFolder() + File.separator + CONFIG_SAVEFILE_NAME); - public static final File OLD_CONFIG_SAVEFILE = new File(SysUtil.getAppData() + File.separator + CONFIG_SAVEFILE_NAME); - - static Gson gson = new GsonBuilder().setPrettyPrinting().create(); - - public Config loadFromFile(File file) throws FileNotFoundException { - - if (!file.exists()) throw new FileNotFoundException(); - - String jsonConfig = SysUtil.loadFileStr(file); - if (jsonConfig.trim().equals("")) return null; - - try { - return gson.fromJson(jsonConfig, Config.class); - } catch (Exception ex) { - Log.info("ConfigLoader", "Gson exception while parsing config file", ex); - return null; - } - - } - - public Config loadFromFile() throws FileNotFoundException { - SysUtil.migrateFile(OLD_CONFIG_SAVEFILE, CONFIG_SAVEFILE); - return loadFromFile(CONFIG_SAVEFILE); - } - - public void saveToFile(File file, Config conf) { - String jsonConfig = gson.toJson(conf); - SysUtil.saveFileStr(file, jsonConfig); - } - - public void saveToFile(Config conf) { - saveToFile(CONFIG_SAVEFILE, conf); - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.config; + +import com.github.serivesmejia.eocvsim.util.Log; +import com.github.serivesmejia.eocvsim.util.SysUtil; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.io.File; +import java.io.FileNotFoundException; + +public class ConfigLoader { + + public static final String CONFIG_SAVEFILE_NAME = "eocvsim_config.json"; + + public static final File CONFIG_SAVEFILE = new File(SysUtil.getEOCVSimFolder() + File.separator + CONFIG_SAVEFILE_NAME); + public static final File OLD_CONFIG_SAVEFILE = new File(SysUtil.getAppData() + File.separator + CONFIG_SAVEFILE_NAME); + + static Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + public Config loadFromFile(File file) throws FileNotFoundException { + + if (!file.exists()) throw new FileNotFoundException(); + + String jsonConfig = SysUtil.loadFileStr(file); + if (jsonConfig.trim().equals("")) return null; + + try { + return gson.fromJson(jsonConfig, Config.class); + } catch (Exception ex) { + Log.info("ConfigLoader", "Gson exception while parsing config file", ex); + return null; + } + + } + + public Config loadFromFile() throws FileNotFoundException { + SysUtil.migrateFile(OLD_CONFIG_SAVEFILE, CONFIG_SAVEFILE); + return loadFromFile(CONFIG_SAVEFILE); + } + + public void saveToFile(File file, Config conf) { + String jsonConfig = gson.toJson(conf); + SysUtil.saveFileStr(file, jsonConfig); + } + + public void saveToFile(Config conf) { + saveToFile(CONFIG_SAVEFILE, conf); + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/config/ConfigManager.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/config/ConfigManager.java index 497dd0b1..852a111a 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/config/ConfigManager.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/config/ConfigManager.java @@ -1,61 +1,61 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.config; - -import com.github.serivesmejia.eocvsim.util.Log; - -public class ConfigManager { - - public final ConfigLoader configLoader = new ConfigLoader(); - private Config config; - - public void init() { - Log.info("ConfigManager", "Initializing..."); - - try { - config = configLoader.loadFromFile(); - if (config == null) { - Log.error("ConfigManager", "Error while parsing config file, it will be replaced and fixed, but the user configurations will be reset"); - throw new NullPointerException(); //for it to be caught later and handle the creation of a new config - } else { - Log.info("ConfigManager", "Loaded config from file successfully"); - } - } catch (Exception ex) { //handles FileNotFoundException & a NullPointerException thrown above - config = new Config(); - Log.info("ConfigManager", "Creating config file..."); - configLoader.saveToFile(config); - } - - Log.blank(); - } - - public void saveToFile() { - configLoader.saveToFile(config); - } - - public Config getConfig() { - return config; - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.config; + +import com.github.serivesmejia.eocvsim.util.Log; + +public class ConfigManager { + + public final ConfigLoader configLoader = new ConfigLoader(); + private Config config; + + public void init() { + Log.info("ConfigManager", "Initializing..."); + + try { + config = configLoader.loadFromFile(); + if (config == null) { + Log.error("ConfigManager", "Error while parsing config file, it will be replaced and fixed, but the user configurations will be reset"); + throw new NullPointerException(); //for it to be caught later and handle the creation of a new config + } else { + Log.info("ConfigManager", "Loaded config from file successfully"); + } + } catch (Exception ex) { //handles FileNotFoundException & a NullPointerException thrown above + config = new Config(); + Log.info("ConfigManager", "Creating config file..."); + configLoader.saveToFile(config); + } + + Log.blank(); + } + + public void saveToFile() { + configLoader.saveToFile(config); + } + + public Config getConfig() { + return config; + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/DialogFactory.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/DialogFactory.java index 9c495aa2..d4122d6a 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/DialogFactory.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/DialogFactory.java @@ -1,208 +1,208 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.gui.dialog.*; -import com.github.serivesmejia.eocvsim.gui.dialog.SplashScreen; -import com.github.serivesmejia.eocvsim.gui.dialog.source.CreateCameraSource; -import com.github.serivesmejia.eocvsim.gui.dialog.source.CreateImageSource; -import com.github.serivesmejia.eocvsim.gui.dialog.source.CreateSource; -import com.github.serivesmejia.eocvsim.gui.dialog.source.CreateVideoSource; -import com.github.serivesmejia.eocvsim.input.SourceType; -import com.github.serivesmejia.eocvsim.util.event.EventHandler; - -import javax.swing.*; -import javax.swing.filechooser.FileFilter; -import javax.swing.filechooser.FileNameExtensionFilter; -import java.awt.*; -import java.io.File; -import java.util.ArrayList; - -public class DialogFactory { - - private DialogFactory() { } - - public static FileChooser createFileChooser(Component parent, FileChooser.Mode mode, FileFilter... filters) { - FileChooser fileChooser = new FileChooser(parent, mode, filters); - invokeLater(fileChooser::init); - return fileChooser; - } - - public static FileChooser createFileChooser(Component parent, FileFilter... filters) { - return createFileChooser(parent, null, filters); - } - - public static FileChooser createFileChooser(Component parent, FileChooser.Mode mode) { - return createFileChooser(parent, mode, new FileFilter[0]); - } - - public static FileChooser createFileChooser(Component parent) { - return createFileChooser(parent, null, new FileFilter[0]); - } - - public static void createSourceDialog(EOCVSim eocvSim, - SourceType type, - File initialFile) { - invokeLater(() -> { - switch (type) { - case IMAGE: - new CreateImageSource(eocvSim.visualizer.frame, eocvSim, initialFile); - break; - case CAMERA: - new CreateCameraSource(eocvSim.visualizer.frame, eocvSim); - break; - case VIDEO: - new CreateVideoSource(eocvSim.visualizer.frame, eocvSim, initialFile); - } - }); - } - - public static void createSourceDialog(EOCVSim eocvSim, SourceType type) { - createSourceDialog(eocvSim, type, null); - } - - public static void createSourceDialog(EOCVSim eocvSim) { - invokeLater(() -> new CreateSource(eocvSim.visualizer.frame, eocvSim)); - } - - public static void createConfigDialog(EOCVSim eocvSim) { - invokeLater(() -> new Configuration(eocvSim.visualizer.frame, eocvSim)); - } - - public static void createAboutDialog(EOCVSim eocvSim) { - invokeLater(() -> new About(eocvSim.visualizer.frame, eocvSim)); - } - - public static void createOutput(EOCVSim eocvSim, boolean wasManuallyOpened) { - invokeLater(() -> { - if(!Output.Companion.isAlreadyOpened()) - new Output(eocvSim.visualizer.frame, eocvSim, Output.Companion.getLatestIndex(), wasManuallyOpened); - }); - } - - public static void createOutput(EOCVSim eocvSim) { - createOutput(eocvSim, false); - } - - public static void createBuildOutput(EOCVSim eocvSim) { - invokeLater(() -> { - if(!Output.Companion.isAlreadyOpened()) - new Output(eocvSim.visualizer.frame, eocvSim, 1); - }); - } - - public static void createPipelineOutput(EOCVSim eocvSim) { - invokeLater(() -> { - if(!Output.Companion.isAlreadyOpened()) - new Output(eocvSim.visualizer.frame, eocvSim, 0); - }); - } - - public static void createSplashScreen(EventHandler closeHandler) { - invokeLater(() -> new SplashScreen(closeHandler)); - } - - public static FileAlreadyExists.UserChoice createFileAlreadyExistsDialog(EOCVSim eocvSim) { - return new FileAlreadyExists(eocvSim.visualizer.frame, eocvSim).run(); - } - - private static void invokeLater(Runnable runn) { - SwingUtilities.invokeLater(runn); - } - - public static class FileChooser { - - private final JFileChooser chooser; - private final Component parent; - - private final Mode mode; - - private final ArrayList closeListeners = new ArrayList<>(); - - public FileChooser(Component parent, Mode mode, FileFilter... filters) { - - if (mode == null) mode = Mode.FILE_SELECT; - - chooser = new JFileChooser(); - - this.parent = parent; - this.mode = mode; - - if (mode == Mode.DIRECTORY_SELECT) { - chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); - // disable the "All files" option. - chooser.setAcceptAllFileFilterUsed(false); - } - - - if (filters != null) { - for (FileFilter filter : filters) { - if(filter != null) chooser.addChoosableFileFilter(filter); - } - if(filters.length > 0) { - chooser.setFileFilter(filters[0]); - } - } - - } - - protected void init() { - - int returnVal; - - if (mode == Mode.SAVE_FILE_SELECT) { - returnVal = chooser.showSaveDialog(parent); - } else { - returnVal = chooser.showOpenDialog(parent); - } - - executeCloseListeners(returnVal, chooser.getSelectedFile(), chooser.getFileFilter()); - - } - - public void addCloseListener(FileChooserCloseListener listener) { - this.closeListeners.add(listener); - } - - private void executeCloseListeners(int OPTION, File selectedFile, FileFilter selectedFileFilter) { - for (FileChooserCloseListener listener : closeListeners) { - listener.onClose(OPTION, selectedFile, selectedFileFilter); - } - } - - public void close() { - chooser.setVisible(false); - executeCloseListeners(JFileChooser.CANCEL_OPTION, new File(""), new FileNameExtensionFilter("", "")); - } - - public enum Mode {FILE_SELECT, DIRECTORY_SELECT, SAVE_FILE_SELECT} - - public interface FileChooserCloseListener { - void onClose(int OPTION, File selectedFile, FileFilter selectedFileFilter); - } - - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.gui.dialog.*; +import com.github.serivesmejia.eocvsim.gui.dialog.SplashScreen; +import com.github.serivesmejia.eocvsim.gui.dialog.source.CreateCameraSource; +import com.github.serivesmejia.eocvsim.gui.dialog.source.CreateImageSource; +import com.github.serivesmejia.eocvsim.gui.dialog.source.CreateSource; +import com.github.serivesmejia.eocvsim.gui.dialog.source.CreateVideoSource; +import com.github.serivesmejia.eocvsim.input.SourceType; +import com.github.serivesmejia.eocvsim.util.event.EventHandler; + +import javax.swing.*; +import javax.swing.filechooser.FileFilter; +import javax.swing.filechooser.FileNameExtensionFilter; +import java.awt.*; +import java.io.File; +import java.util.ArrayList; + +public class DialogFactory { + + private DialogFactory() { } + + public static FileChooser createFileChooser(Component parent, FileChooser.Mode mode, FileFilter... filters) { + FileChooser fileChooser = new FileChooser(parent, mode, filters); + invokeLater(fileChooser::init); + return fileChooser; + } + + public static FileChooser createFileChooser(Component parent, FileFilter... filters) { + return createFileChooser(parent, null, filters); + } + + public static FileChooser createFileChooser(Component parent, FileChooser.Mode mode) { + return createFileChooser(parent, mode, new FileFilter[0]); + } + + public static FileChooser createFileChooser(Component parent) { + return createFileChooser(parent, null, new FileFilter[0]); + } + + public static void createSourceDialog(EOCVSim eocvSim, + SourceType type, + File initialFile) { + invokeLater(() -> { + switch (type) { + case IMAGE: + new CreateImageSource(eocvSim.visualizer.frame, eocvSim, initialFile); + break; + case CAMERA: + new CreateCameraSource(eocvSim.visualizer.frame, eocvSim); + break; + case VIDEO: + new CreateVideoSource(eocvSim.visualizer.frame, eocvSim, initialFile); + } + }); + } + + public static void createSourceDialog(EOCVSim eocvSim, SourceType type) { + createSourceDialog(eocvSim, type, null); + } + + public static void createSourceDialog(EOCVSim eocvSim) { + invokeLater(() -> new CreateSource(eocvSim.visualizer.frame, eocvSim)); + } + + public static void createConfigDialog(EOCVSim eocvSim) { + invokeLater(() -> new Configuration(eocvSim.visualizer.frame, eocvSim)); + } + + public static void createAboutDialog(EOCVSim eocvSim) { + invokeLater(() -> new About(eocvSim.visualizer.frame, eocvSim)); + } + + public static void createOutput(EOCVSim eocvSim, boolean wasManuallyOpened) { + invokeLater(() -> { + if(!Output.Companion.isAlreadyOpened()) + new Output(eocvSim.visualizer.frame, eocvSim, Output.Companion.getLatestIndex(), wasManuallyOpened); + }); + } + + public static void createOutput(EOCVSim eocvSim) { + createOutput(eocvSim, false); + } + + public static void createBuildOutput(EOCVSim eocvSim) { + invokeLater(() -> { + if(!Output.Companion.isAlreadyOpened()) + new Output(eocvSim.visualizer.frame, eocvSim, 1); + }); + } + + public static void createPipelineOutput(EOCVSim eocvSim) { + invokeLater(() -> { + if(!Output.Companion.isAlreadyOpened()) + new Output(eocvSim.visualizer.frame, eocvSim, 0); + }); + } + + public static void createSplashScreen(EventHandler closeHandler) { + invokeLater(() -> new SplashScreen(closeHandler)); + } + + public static FileAlreadyExists.UserChoice createFileAlreadyExistsDialog(EOCVSim eocvSim) { + return new FileAlreadyExists(eocvSim.visualizer.frame, eocvSim).run(); + } + + private static void invokeLater(Runnable runn) { + SwingUtilities.invokeLater(runn); + } + + public static class FileChooser { + + private final JFileChooser chooser; + private final Component parent; + + private final Mode mode; + + private final ArrayList closeListeners = new ArrayList<>(); + + public FileChooser(Component parent, Mode mode, FileFilter... filters) { + + if (mode == null) mode = Mode.FILE_SELECT; + + chooser = new JFileChooser(); + + this.parent = parent; + this.mode = mode; + + if (mode == Mode.DIRECTORY_SELECT) { + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + // disable the "All files" option. + chooser.setAcceptAllFileFilterUsed(false); + } + + + if (filters != null) { + for (FileFilter filter : filters) { + if(filter != null) chooser.addChoosableFileFilter(filter); + } + if(filters.length > 0) { + chooser.setFileFilter(filters[0]); + } + } + + } + + protected void init() { + + int returnVal; + + if (mode == Mode.SAVE_FILE_SELECT) { + returnVal = chooser.showSaveDialog(parent); + } else { + returnVal = chooser.showOpenDialog(parent); + } + + executeCloseListeners(returnVal, chooser.getSelectedFile(), chooser.getFileFilter()); + + } + + public void addCloseListener(FileChooserCloseListener listener) { + this.closeListeners.add(listener); + } + + private void executeCloseListeners(int OPTION, File selectedFile, FileFilter selectedFileFilter) { + for (FileChooserCloseListener listener : closeListeners) { + listener.onClose(OPTION, selectedFile, selectedFileFilter); + } + } + + public void close() { + chooser.setVisible(false); + executeCloseListeners(JFileChooser.CANCEL_OPTION, new File(""), new FileNameExtensionFilter("", "")); + } + + public enum Mode {FILE_SELECT, DIRECTORY_SELECT, SAVE_FILE_SELECT} + + public interface FileChooserCloseListener { + void onClose(int OPTION, File selectedFile, FileFilter selectedFileFilter); + } + + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt index 7a981bf2..06cfc6ba 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Icons.kt @@ -1,144 +1,144 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui - -import com.github.serivesmejia.eocvsim.gui.util.GuiUtil -import com.github.serivesmejia.eocvsim.util.Log -import java.awt.image.BufferedImage -import java.util.NoSuchElementException -import javax.swing.ImageIcon - -object Icons { - - private val bufferedImages = HashMap() - - private val icons = HashMap() - private val resizedIcons = HashMap() - - private val futureIcons = mutableListOf() - - private var colorsInverted = false - - private const val TAG = "Icons" - - init { - addFutureImage("ico_eocvsim", "/images/icon/ico_eocvsim.png", false) - - addFutureImage("ico_img", "/images/icon/ico_img.png") - addFutureImage("ico_cam", "/images/icon/ico_cam.png") - addFutureImage("ico_vid", "/images/icon/ico_vid.png") - - addFutureImage("ico_config", "/images/icon/ico_config.png") - addFutureImage("ico_slider", "/images/icon/ico_slider.png") - addFutureImage("ico_textbox", "/images/icon/ico_textbox.png") - addFutureImage("ico_colorpick", "/images/icon/ico_colorpick.png") - - addFutureImage("ico_gears", "/images/icon/ico_gears.png") - addFutureImage("ico_hammer", "/images/icon/ico_hammer.png") - - addFutureImage("ico_colorpick_pointer", "/images/icon/ico_colorpick_pointer.png") - } - - fun getImage(name: String): ImageIcon { - for(futureIcon in futureIcons.toTypedArray()) { - if(futureIcon.name == name) { - Log.info(TAG, "Loading future icon $name") - addImage(futureIcon.name, futureIcon.resourcePath, futureIcon.allowInvert) - - futureIcons.remove(futureIcon) - } - } - - if(!icons.containsKey(name)) { - throw NoSuchElementException("Image $name is not loaded into memory") - } - return icons[name]!! - } - - fun lazyGetImageResized(name: String, width: Int, height: Int) = lazy { - getImageResized(name, width, height) - } - - fun getImageResized(name: String, width: Int, height: Int): ImageIcon { - //determines the icon name from the: - //name, widthxheight, is inverted or is original - val resIconName = "$name-${width}x${height}${ - if(colorsInverted) { - "-inverted" - } else { - "" - } - }" - - val icon = if(resizedIcons.contains(resIconName)) { - resizedIcons[resIconName] - } else { - resizedIcons[resIconName] = GuiUtil.scaleImage(getImage(name), width, height) - resizedIcons[resIconName] - } - - return icon!! - } - - fun addFutureImage(name: String, path: String, allowInvert: Boolean = true) = futureIcons.add( - FutureIcon(name, path, allowInvert) - ) - - fun addImage(name: String, path: String, allowInvert: Boolean = true) { - val buffImg = GuiUtil.loadBufferedImage(path) - if(colorsInverted && allowInvert) { - GuiUtil.invertBufferedImageColors(buffImg) - } - - bufferedImages[name] = Image(buffImg, allowInvert) - icons[name] = ImageIcon(buffImg) - } - - fun setDark(dark: Boolean) { - if(dark) { - if(!colorsInverted) { - invertAll() - colorsInverted = true - } - } else { - if(colorsInverted) { - invertAll() - colorsInverted = false - } - } - } - - private fun invertAll() { - for((_, image) in bufferedImages) { - if(image.allowInvert) { - GuiUtil.invertBufferedImageColors(image.img) - } - } - } - - data class Image(val img: BufferedImage, val allowInvert: Boolean) - - data class FutureIcon(val name: String, val resourcePath: String, val allowInvert: Boolean) - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui + +import com.github.serivesmejia.eocvsim.gui.util.GuiUtil +import com.github.serivesmejia.eocvsim.util.Log +import java.awt.image.BufferedImage +import java.util.NoSuchElementException +import javax.swing.ImageIcon + +object Icons { + + private val bufferedImages = HashMap() + + private val icons = HashMap() + private val resizedIcons = HashMap() + + private val futureIcons = mutableListOf() + + private var colorsInverted = false + + private const val TAG = "Icons" + + init { + addFutureImage("ico_eocvsim", "/images/icon/ico_eocvsim.png", false) + + addFutureImage("ico_img", "/images/icon/ico_img.png") + addFutureImage("ico_cam", "/images/icon/ico_cam.png") + addFutureImage("ico_vid", "/images/icon/ico_vid.png") + + addFutureImage("ico_config", "/images/icon/ico_config.png") + addFutureImage("ico_slider", "/images/icon/ico_slider.png") + addFutureImage("ico_textbox", "/images/icon/ico_textbox.png") + addFutureImage("ico_colorpick", "/images/icon/ico_colorpick.png") + + addFutureImage("ico_gears", "/images/icon/ico_gears.png") + addFutureImage("ico_hammer", "/images/icon/ico_hammer.png") + + addFutureImage("ico_colorpick_pointer", "/images/icon/ico_colorpick_pointer.png") + } + + fun getImage(name: String): ImageIcon { + for(futureIcon in futureIcons.toTypedArray()) { + if(futureIcon.name == name) { + Log.info(TAG, "Loading future icon $name") + addImage(futureIcon.name, futureIcon.resourcePath, futureIcon.allowInvert) + + futureIcons.remove(futureIcon) + } + } + + if(!icons.containsKey(name)) { + throw NoSuchElementException("Image $name is not loaded into memory") + } + return icons[name]!! + } + + fun lazyGetImageResized(name: String, width: Int, height: Int) = lazy { + getImageResized(name, width, height) + } + + fun getImageResized(name: String, width: Int, height: Int): ImageIcon { + //determines the icon name from the: + //name, widthxheight, is inverted or is original + val resIconName = "$name-${width}x${height}${ + if(colorsInverted) { + "-inverted" + } else { + "" + } + }" + + val icon = if(resizedIcons.contains(resIconName)) { + resizedIcons[resIconName] + } else { + resizedIcons[resIconName] = GuiUtil.scaleImage(getImage(name), width, height) + resizedIcons[resIconName] + } + + return icon!! + } + + fun addFutureImage(name: String, path: String, allowInvert: Boolean = true) = futureIcons.add( + FutureIcon(name, path, allowInvert) + ) + + fun addImage(name: String, path: String, allowInvert: Boolean = true) { + val buffImg = GuiUtil.loadBufferedImage(path) + if(colorsInverted && allowInvert) { + GuiUtil.invertBufferedImageColors(buffImg) + } + + bufferedImages[name] = Image(buffImg, allowInvert) + icons[name] = ImageIcon(buffImg) + } + + fun setDark(dark: Boolean) { + if(dark) { + if(!colorsInverted) { + invertAll() + colorsInverted = true + } + } else { + if(colorsInverted) { + invertAll() + colorsInverted = false + } + } + } + + private fun invertAll() { + for((_, image) in bufferedImages) { + if(image.allowInvert) { + GuiUtil.invertBufferedImageColors(image.img) + } + } + } + + data class Image(val img: BufferedImage, val allowInvert: Boolean) + + data class FutureIcon(val name: String, val resourcePath: String, val allowInvert: Boolean) + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java index c75fc73d..c0ea5e92 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java @@ -1,592 +1,592 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui; - -import com.formdev.flatlaf.FlatLaf; -import com.github.serivesmejia.eocvsim.Build; -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.gui.component.Viewport; -import com.github.serivesmejia.eocvsim.gui.component.tuner.ColorPicker; -import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; -import com.github.serivesmejia.eocvsim.gui.component.visualizer.InputSourceDropTarget; -import com.github.serivesmejia.eocvsim.gui.component.visualizer.SourceSelectorPanel; -import com.github.serivesmejia.eocvsim.gui.component.visualizer.TelemetryPanel; -import com.github.serivesmejia.eocvsim.gui.component.visualizer.TopMenuBar; -import com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline.PipelineSelectorPanel; -import com.github.serivesmejia.eocvsim.gui.theme.Theme; -import com.github.serivesmejia.eocvsim.gui.util.ReflectTaskbar; -import com.github.serivesmejia.eocvsim.pipeline.compiler.PipelineCompiler; -import com.github.serivesmejia.eocvsim.util.Log; -import com.github.serivesmejia.eocvsim.util.event.EventHandler; -import com.github.serivesmejia.eocvsim.workspace.util.template.GradleWorkspaceTemplate; -import kotlin.Unit; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import java.awt.*; -import java.awt.event.*; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -public class Visualizer { - - public final EventHandler onInitFinished = new EventHandler("OnVisualizerInitFinish"); - - public final ArrayList pleaseWaitDialogs = new ArrayList<>(); - - public final ArrayList childFrames = new ArrayList<>(); - public final ArrayList childDialogs = new ArrayList<>(); - - private final EOCVSim eocvSim; - public JFrame frame; - - public Viewport viewport = null; - public TopMenuBar menuBar = null; - public JPanel tunerMenuPanel = new JPanel(); - - public JScrollPane imgScrollPane = null; - - public JPanel rightContainer = null; - public JSplitPane globalSplitPane = null; - public JSplitPane imageTunerSplitPane = null; - - public PipelineSelectorPanel pipelineSelectorPanel = null; - public SourceSelectorPanel sourceSelectorPanel = null; - public TelemetryPanel telemetryPanel; - - private String title = "EasyOpenCV Simulator v" + Build.standardVersionString; - private String titleMsg = "No pipeline"; - private String beforeTitle = ""; - private String beforeTitleMsg = ""; - - public ColorPicker colorPicker = null; - - //stuff for zooming handling - private volatile boolean isCtrlPressed = false; - - private volatile boolean hasFinishedInitializing = false; - - public Visualizer(EOCVSim eocvSim) { - this.eocvSim = eocvSim; - } - - public void init(Theme theme) { - if(ReflectTaskbar.INSTANCE.isUsable()){ - try { - //set icon for mac os (and other systems which do support this method) - ReflectTaskbar.INSTANCE.setIconImage(Icons.INSTANCE.getImage("ico_eocvsim").getImage()); - } catch (final UnsupportedOperationException e) { - Log.warn("Visualizer", "Setting the Taskbar icon image is not supported on this platform"); - } catch (final SecurityException e) { - Log.error("Visualizer", "Security exception while setting TaskBar icon", e); - } - } - - try { - theme.install(); - } catch (Exception e) { - Log.error("Visualizer", "Failed to install theme " + theme.name(), e); - } - - Icons.INSTANCE.setDark(FlatLaf.isLafDark()); - - if(Build.isDev) { - title += "-dev "; - } - - //instantiate all swing elements after theme installation - frame = new JFrame(); - viewport = new Viewport(eocvSim, eocvSim.getConfig().pipelineMaxFps.getFps()); - - menuBar = new TopMenuBar(this, eocvSim); - - tunerMenuPanel = new JPanel(); - - pipelineSelectorPanel = new PipelineSelectorPanel(eocvSim); - sourceSelectorPanel = new SourceSelectorPanel(eocvSim); - telemetryPanel = new TelemetryPanel(); - - rightContainer = new JPanel(); - - /* - * TOP MENU BAR - */ - - frame.setJMenuBar(menuBar); - - /* - * IMG VISUALIZER & SCROLL PANE - */ - - imgScrollPane = new JScrollPane(viewport); - - imgScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); - imgScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); - - imgScrollPane.getHorizontalScrollBar().setUnitIncrement(16); - imgScrollPane.getVerticalScrollBar().setUnitIncrement(16); - - rightContainer.setLayout(new BoxLayout(rightContainer, BoxLayout.Y_AXIS)); - - /* - * PIPELINE SELECTOR - */ - pipelineSelectorPanel.setBorder(new EmptyBorder(0, 20, 0, 20)); - rightContainer.add(pipelineSelectorPanel); - - /* - * SOURCE SELECTOR - */ - sourceSelectorPanel.setBorder(new EmptyBorder(0, 20, 0, 20)); - rightContainer.add(sourceSelectorPanel); - - /* - * TELEMETRY - */ - telemetryPanel.setBorder(new EmptyBorder(0, 20, 20, 20)); - rightContainer.add(telemetryPanel); - - /* - * SPLIT - */ - - //left side, image scroll & tuner menu split panel - imageTunerSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, imgScrollPane, tunerMenuPanel); - - imageTunerSplitPane.setResizeWeight(1); - imageTunerSplitPane.setOneTouchExpandable(false); - imageTunerSplitPane.setContinuousLayout(true); - - //global - globalSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, imageTunerSplitPane, rightContainer); - - globalSplitPane.setResizeWeight(1); - globalSplitPane.setOneTouchExpandable(false); - globalSplitPane.setContinuousLayout(true); - - globalSplitPane.setDropTarget(new InputSourceDropTarget(eocvSim)); - - frame.add(globalSplitPane, BorderLayout.CENTER); - - //initialize other various stuff of the frame - frame.setSize(780, 645); - frame.setMinimumSize(frame.getSize()); - frame.setTitle("EasyOpenCV Simulator - No Pipeline"); - - frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); - - frame.setIconImage(Icons.INSTANCE.getImage("ico_eocvsim").getImage()); - - frame.setLocationRelativeTo(null); - frame.setExtendedState(JFrame.MAXIMIZED_BOTH); - frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - - globalSplitPane.setDividerLocation(1070); - - colorPicker = new ColorPicker(viewport.image); - - frame.setVisible(true); - - onInitFinished.run(); - onInitFinished.setCallRightAway(true); - - registerListeners(); - - hasFinishedInitializing = true; - - if(!PipelineCompiler.Companion.getIS_USABLE()) { - compilingUnsupported(); - } - } - - public void initAsync(Theme simTheme) { - SwingUtilities.invokeLater(() -> init(simTheme)); - } - - private void registerListeners() { - - frame.addWindowListener(new WindowAdapter() { - public void windowClosing(WindowEvent e) { - eocvSim.onMainUpdate.doOnce((Runnable) eocvSim::destroy); - } - }); - - //handling onViewportTapped evts - viewport.addMouseListener(new MouseAdapter() { - public void mouseClicked(MouseEvent e) { - if(!colorPicker.isPicking()) - eocvSim.pipelineManager.callViewportTapped(); - } - }); - - //VIEWPORT RESIZE HANDLING - imgScrollPane.addMouseWheelListener(e -> { - if (isCtrlPressed) { //check if control key is pressed - double scale = viewport.getViewportScale() - (0.15 * e.getPreciseWheelRotation()); - viewport.setViewportScale(scale); - } - }); - - //listening for keyboard presses and releases, to check if ctrl key was pressed or released (handling zoom) - KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(ke -> { - switch (ke.getID()) { - case KeyEvent.KEY_PRESSED: - if (ke.getKeyCode() == KeyEvent.VK_CONTROL) { - isCtrlPressed = true; - imgScrollPane.setWheelScrollingEnabled(false); //lock scrolling if ctrl is pressed - } - break; - case KeyEvent.KEY_RELEASED: - if (ke.getKeyCode() == KeyEvent.VK_CONTROL) { - isCtrlPressed = false; - imgScrollPane.setWheelScrollingEnabled(true); //unlock - } - break; - } - return false; //idk let's just return false 'cause keyboard input doesn't work otherwise - }); - - //resizes all three JLists in right panel to make buttons visible in smaller resolutions - frame.addComponentListener(new ComponentAdapter() { - @Override - public void componentResized(ComponentEvent evt) { - double ratioH = frame.getSize().getHeight() / 645; - - double fontSize = 17 * ratioH; - Font font = pipelineSelectorPanel.getPipelineSelectorLabel().getFont().deriveFont((float)fontSize); - - pipelineSelectorPanel.getPipelineSelectorLabel().setFont(font); - pipelineSelectorPanel.revalAndRepaint(); - - sourceSelectorPanel.getSourceSelectorLabel().setFont(font); - sourceSelectorPanel.revalAndRepaint(); - - telemetryPanel.getTelemetryLabel().setFont(font); - telemetryPanel.revalAndRepaint(); - - rightContainer.revalidate(); - rightContainer.repaint(); - } - }); - - // stop color-picking mode when changing pipeline - // TODO: find out why this breaks everything????? - // eocvSim.pipelineManager.onPipelineChange.doPersistent(() -> colorPicker.stopPicking()); - } - - public boolean hasFinishedInit() { return hasFinishedInitializing; } - - public void waitForFinishingInit() { - while (!hasFinishedInitializing) { - Thread.yield(); - } - } - - public void close() { - SwingUtilities.invokeLater(() -> { - frame.setVisible(false); - viewport.stop(); - - //close all asyncpleasewait dialogs - for (AsyncPleaseWaitDialog dialog : pleaseWaitDialogs) { - if (dialog != null) { - dialog.destroyDialog(); - } - } - - pleaseWaitDialogs.clear(); - - //close all opened frames - for (JFrame frame : childFrames) { - if (frame != null && frame.isVisible()) { - frame.setVisible(false); - frame.dispose(); - } - } - - childFrames.clear(); - - //close all opened dialogs - for (JDialog dialog : childDialogs) { - if (dialog != null && dialog.isVisible()) { - dialog.setVisible(false); - dialog.dispose(); - } - } - - childDialogs.clear(); - frame.dispose(); - viewport.flush(); - }); - } - - private void setFrameTitle(String title, String titleMsg) { - frame.setTitle(title + " - " + titleMsg); - } - - public void setTitle(String title) { - this.title = title; - if (!beforeTitle.equals(title)) setFrameTitle(title, titleMsg); - beforeTitle = title; - } - - public void setTitleMessage(String titleMsg) { - this.titleMsg = titleMsg; - if (!beforeTitleMsg.equals(title)) setFrameTitle(title, titleMsg); - beforeTitleMsg = titleMsg; - } - - public void updateTunerFields(List fields) { - tunerMenuPanel.removeAll(); - tunerMenuPanel.repaint(); - - for (TunableFieldPanel fieldPanel : fields) { - tunerMenuPanel.add(fieldPanel); - fieldPanel.showFieldPanel(); - } - } - - public void asyncCompilePipelines() { - if(PipelineCompiler.Companion.getIS_USABLE()) { - menuBar.workspCompile.setEnabled(false); - pipelineSelectorPanel.getButtonsPanel().getPipelineCompileBtt().setEnabled(false); - - eocvSim.pipelineManager.compiledPipelineManager.asyncCompile(true, (result) -> { - menuBar.workspCompile.setEnabled(true); - pipelineSelectorPanel.getButtonsPanel().getPipelineCompileBtt().setEnabled(true); - - return Unit.INSTANCE; - }); - } else { - compilingUnsupported(); - } - } - - public void compilingUnsupported() { - asyncPleaseWaitDialog( - "Runtime compiling is not supported on this JVM", - "For further info, check the EOCV-Sim GitHub repo", - "Close", - new Dimension(320, 160), - true, true - ); - } - - public void selectPipelinesWorkspace() { - DialogFactory.createFileChooser( - frame, DialogFactory.FileChooser.Mode.DIRECTORY_SELECT - ).addCloseListener((OPTION, selectedFile, selectedFileFilter) -> { - if (OPTION == JFileChooser.APPROVE_OPTION) { - if(!selectedFile.exists()) selectedFile.mkdir(); - - eocvSim.onMainUpdate.doOnce(() -> - eocvSim.workspaceManager.setWorkspaceFile(selectedFile) - ); - } - }); - } - - public void createVSCodeWorkspace() { - DialogFactory.createFileChooser(frame, DialogFactory.FileChooser.Mode.DIRECTORY_SELECT) - .addCloseListener((OPTION, selectedFile, selectedFileFilter) -> { - if(OPTION == JFileChooser.APPROVE_OPTION) { - if(!selectedFile.exists()) selectedFile.mkdir(); - - if(selectedFile.isDirectory() && - Objects.requireNonNull(selectedFile.listFiles()).length == 0) { - eocvSim.workspaceManager.createWorkspaceWithTemplateAsync(selectedFile, GradleWorkspaceTemplate.INSTANCE); - } else { - asyncPleaseWaitDialog( - "The selected directory must be empty", "Select an empty directory or create a new one", - "Retry", new Dimension(320, 160), true, true - ).onCancel(this::createVSCodeWorkspace); - } - } - }); - } - - // PLEASE WAIT DIALOGS - - public boolean pleaseWaitDialog(JDialog diag, String message, String subMessage, String cancelBttText, Dimension size, boolean cancellable, AsyncPleaseWaitDialog apwd, boolean isError) { - final JDialog dialog = diag == null ? new JDialog(this.frame) : diag; - - boolean addSubMessage = subMessage != null; - - int rows = 3; - if (!addSubMessage) { - rows--; - } - - dialog.setModal(true); - dialog.setLayout(new GridLayout(rows, 1)); - - if (isError) { - dialog.setTitle("Operation failed"); - } else { - dialog.setTitle("Operation in progress"); - } - - JLabel msg = new JLabel(message); - msg.setHorizontalAlignment(JLabel.CENTER); - msg.setVerticalAlignment(JLabel.CENTER); - - dialog.add(msg); - - JLabel subMsg = null; - if (addSubMessage) { - - subMsg = new JLabel(subMessage); - subMsg.setHorizontalAlignment(JLabel.CENTER); - subMsg.setVerticalAlignment(JLabel.CENTER); - - dialog.add(subMsg); - - } - - JPanel exitBttPanel = new JPanel(new FlowLayout()); - JButton cancelBtt = new JButton(cancelBttText); - - cancelBtt.setEnabled(cancellable); - - exitBttPanel.add(cancelBtt); - - boolean[] cancelled = {false}; - - cancelBtt.addActionListener(e -> { - cancelled[0] = true; - dialog.setVisible(false); - dialog.dispose(); - }); - - dialog.add(exitBttPanel); - - if (apwd != null) { - apwd.msg = msg; - apwd.subMsg = subMsg; - apwd.cancelBtt = cancelBtt; - } - - if(size == null) size = new Dimension(400, 200); - dialog.setSize(size); - - dialog.setLocationRelativeTo(null); - dialog.setResizable(false); - dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); - - dialog.setVisible(true); - - return cancelled[0]; - } - - public void pleaseWaitDialog(JDialog dialog, String message, String subMessage, String cancelBttText, Dimension size, boolean cancellable) { - pleaseWaitDialog(dialog, message, subMessage, cancelBttText, size, cancellable, null, false); - } - - public void pleaseWaitDialog(String message, String subMessage, String cancelBttText, Dimension size, boolean cancellable) { - pleaseWaitDialog(null, message, subMessage, cancelBttText, size, cancellable, null, false); - } - - public AsyncPleaseWaitDialog asyncPleaseWaitDialog(String message, String subMessage, String cancelBttText, Dimension size, boolean cancellable, boolean isError) { - AsyncPleaseWaitDialog rPWD = new AsyncPleaseWaitDialog(message, subMessage, cancelBttText, size, cancellable, isError, eocvSim); - SwingUtilities.invokeLater(rPWD); - - return rPWD; - } - - public AsyncPleaseWaitDialog asyncPleaseWaitDialog(String message, String subMessage, String cancelBttText, Dimension size, boolean cancellable) { - AsyncPleaseWaitDialog rPWD = new AsyncPleaseWaitDialog(message, subMessage, cancelBttText, size, cancellable, false, eocvSim); - SwingUtilities.invokeLater(rPWD); - - return rPWD; - } - - public class AsyncPleaseWaitDialog implements Runnable { - - public volatile JDialog dialog = new JDialog(frame); - - public volatile JLabel msg = null; - public volatile JLabel subMsg = null; - - public volatile JButton cancelBtt = null; - - public volatile boolean wasCancelled = false; - public volatile boolean isError; - - public volatile String initialMessage; - public volatile String initialSubMessage; - - public volatile boolean isDestroyed = false; - - String message; - String subMessage; - String cancelBttText; - - Dimension size; - - boolean cancellable; - - private final ArrayList onCancelRunnables = new ArrayList<>(); - - public AsyncPleaseWaitDialog(String message, String subMessage, String cancelBttText, Dimension size, boolean cancellable, boolean isError, EOCVSim eocvSim) { - this.message = message; - this.subMessage = subMessage; - this.initialMessage = message; - this.initialSubMessage = subMessage; - this.cancelBttText = cancelBttText; - - this.size = size; - this.cancellable = cancellable; - - this.isError = isError; - - eocvSim.visualizer.pleaseWaitDialogs.add(this); - } - - public void onCancel(Runnable runn) { - onCancelRunnables.add(runn); - } - - @Override - public void run() { - wasCancelled = pleaseWaitDialog(dialog, message, subMessage, cancelBttText, size, cancellable, this, isError); - - if (wasCancelled) { - for (Runnable runn : onCancelRunnables) { - runn.run(); - } - } - } - - public void destroyDialog() { - if (!isDestroyed) { - dialog.setVisible(false); - dialog.dispose(); - isDestroyed = true; - } - } - - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui; + +import com.formdev.flatlaf.FlatLaf; +import com.github.serivesmejia.eocvsim.Build; +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.gui.component.Viewport; +import com.github.serivesmejia.eocvsim.gui.component.tuner.ColorPicker; +import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; +import com.github.serivesmejia.eocvsim.gui.component.visualizer.InputSourceDropTarget; +import com.github.serivesmejia.eocvsim.gui.component.visualizer.SourceSelectorPanel; +import com.github.serivesmejia.eocvsim.gui.component.visualizer.TelemetryPanel; +import com.github.serivesmejia.eocvsim.gui.component.visualizer.TopMenuBar; +import com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline.PipelineSelectorPanel; +import com.github.serivesmejia.eocvsim.gui.theme.Theme; +import com.github.serivesmejia.eocvsim.gui.util.ReflectTaskbar; +import com.github.serivesmejia.eocvsim.pipeline.compiler.PipelineCompiler; +import com.github.serivesmejia.eocvsim.util.Log; +import com.github.serivesmejia.eocvsim.util.event.EventHandler; +import com.github.serivesmejia.eocvsim.workspace.util.template.GradleWorkspaceTemplate; +import kotlin.Unit; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.awt.event.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class Visualizer { + + public final EventHandler onInitFinished = new EventHandler("OnVisualizerInitFinish"); + + public final ArrayList pleaseWaitDialogs = new ArrayList<>(); + + public final ArrayList childFrames = new ArrayList<>(); + public final ArrayList childDialogs = new ArrayList<>(); + + private final EOCVSim eocvSim; + public JFrame frame; + + public Viewport viewport = null; + public TopMenuBar menuBar = null; + public JPanel tunerMenuPanel = new JPanel(); + + public JScrollPane imgScrollPane = null; + + public JPanel rightContainer = null; + public JSplitPane globalSplitPane = null; + public JSplitPane imageTunerSplitPane = null; + + public PipelineSelectorPanel pipelineSelectorPanel = null; + public SourceSelectorPanel sourceSelectorPanel = null; + public TelemetryPanel telemetryPanel; + + private String title = "EasyOpenCV Simulator v" + Build.standardVersionString; + private String titleMsg = "No pipeline"; + private String beforeTitle = ""; + private String beforeTitleMsg = ""; + + public ColorPicker colorPicker = null; + + //stuff for zooming handling + private volatile boolean isCtrlPressed = false; + + private volatile boolean hasFinishedInitializing = false; + + public Visualizer(EOCVSim eocvSim) { + this.eocvSim = eocvSim; + } + + public void init(Theme theme) { + if(ReflectTaskbar.INSTANCE.isUsable()){ + try { + //set icon for mac os (and other systems which do support this method) + ReflectTaskbar.INSTANCE.setIconImage(Icons.INSTANCE.getImage("ico_eocvsim").getImage()); + } catch (final UnsupportedOperationException e) { + Log.warn("Visualizer", "Setting the Taskbar icon image is not supported on this platform"); + } catch (final SecurityException e) { + Log.error("Visualizer", "Security exception while setting TaskBar icon", e); + } + } + + try { + theme.install(); + } catch (Exception e) { + Log.error("Visualizer", "Failed to install theme " + theme.name(), e); + } + + Icons.INSTANCE.setDark(FlatLaf.isLafDark()); + + if(Build.isDev) { + title += "-dev "; + } + + //instantiate all swing elements after theme installation + frame = new JFrame(); + viewport = new Viewport(eocvSim, eocvSim.getConfig().pipelineMaxFps.getFps()); + + menuBar = new TopMenuBar(this, eocvSim); + + tunerMenuPanel = new JPanel(); + + pipelineSelectorPanel = new PipelineSelectorPanel(eocvSim); + sourceSelectorPanel = new SourceSelectorPanel(eocvSim); + telemetryPanel = new TelemetryPanel(); + + rightContainer = new JPanel(); + + /* + * TOP MENU BAR + */ + + frame.setJMenuBar(menuBar); + + /* + * IMG VISUALIZER & SCROLL PANE + */ + + imgScrollPane = new JScrollPane(viewport); + + imgScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + imgScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + + imgScrollPane.getHorizontalScrollBar().setUnitIncrement(16); + imgScrollPane.getVerticalScrollBar().setUnitIncrement(16); + + rightContainer.setLayout(new BoxLayout(rightContainer, BoxLayout.Y_AXIS)); + + /* + * PIPELINE SELECTOR + */ + pipelineSelectorPanel.setBorder(new EmptyBorder(0, 20, 0, 20)); + rightContainer.add(pipelineSelectorPanel); + + /* + * SOURCE SELECTOR + */ + sourceSelectorPanel.setBorder(new EmptyBorder(0, 20, 0, 20)); + rightContainer.add(sourceSelectorPanel); + + /* + * TELEMETRY + */ + telemetryPanel.setBorder(new EmptyBorder(0, 20, 20, 20)); + rightContainer.add(telemetryPanel); + + /* + * SPLIT + */ + + //left side, image scroll & tuner menu split panel + imageTunerSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, imgScrollPane, tunerMenuPanel); + + imageTunerSplitPane.setResizeWeight(1); + imageTunerSplitPane.setOneTouchExpandable(false); + imageTunerSplitPane.setContinuousLayout(true); + + //global + globalSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, imageTunerSplitPane, rightContainer); + + globalSplitPane.setResizeWeight(1); + globalSplitPane.setOneTouchExpandable(false); + globalSplitPane.setContinuousLayout(true); + + globalSplitPane.setDropTarget(new InputSourceDropTarget(eocvSim)); + + frame.add(globalSplitPane, BorderLayout.CENTER); + + //initialize other various stuff of the frame + frame.setSize(780, 645); + frame.setMinimumSize(frame.getSize()); + frame.setTitle("EasyOpenCV Simulator - No Pipeline"); + + frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + + frame.setIconImage(Icons.INSTANCE.getImage("ico_eocvsim").getImage()); + + frame.setLocationRelativeTo(null); + frame.setExtendedState(JFrame.MAXIMIZED_BOTH); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + globalSplitPane.setDividerLocation(1070); + + colorPicker = new ColorPicker(viewport.image); + + frame.setVisible(true); + + onInitFinished.run(); + onInitFinished.setCallRightAway(true); + + registerListeners(); + + hasFinishedInitializing = true; + + if(!PipelineCompiler.Companion.getIS_USABLE()) { + compilingUnsupported(); + } + } + + public void initAsync(Theme simTheme) { + SwingUtilities.invokeLater(() -> init(simTheme)); + } + + private void registerListeners() { + + frame.addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent e) { + eocvSim.onMainUpdate.doOnce((Runnable) eocvSim::destroy); + } + }); + + //handling onViewportTapped evts + viewport.addMouseListener(new MouseAdapter() { + public void mouseClicked(MouseEvent e) { + if(!colorPicker.isPicking()) + eocvSim.pipelineManager.callViewportTapped(); + } + }); + + //VIEWPORT RESIZE HANDLING + imgScrollPane.addMouseWheelListener(e -> { + if (isCtrlPressed) { //check if control key is pressed + double scale = viewport.getViewportScale() - (0.15 * e.getPreciseWheelRotation()); + viewport.setViewportScale(scale); + } + }); + + //listening for keyboard presses and releases, to check if ctrl key was pressed or released (handling zoom) + KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(ke -> { + switch (ke.getID()) { + case KeyEvent.KEY_PRESSED: + if (ke.getKeyCode() == KeyEvent.VK_CONTROL) { + isCtrlPressed = true; + imgScrollPane.setWheelScrollingEnabled(false); //lock scrolling if ctrl is pressed + } + break; + case KeyEvent.KEY_RELEASED: + if (ke.getKeyCode() == KeyEvent.VK_CONTROL) { + isCtrlPressed = false; + imgScrollPane.setWheelScrollingEnabled(true); //unlock + } + break; + } + return false; //idk let's just return false 'cause keyboard input doesn't work otherwise + }); + + //resizes all three JLists in right panel to make buttons visible in smaller resolutions + frame.addComponentListener(new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent evt) { + double ratioH = frame.getSize().getHeight() / 645; + + double fontSize = 17 * ratioH; + Font font = pipelineSelectorPanel.getPipelineSelectorLabel().getFont().deriveFont((float)fontSize); + + pipelineSelectorPanel.getPipelineSelectorLabel().setFont(font); + pipelineSelectorPanel.revalAndRepaint(); + + sourceSelectorPanel.getSourceSelectorLabel().setFont(font); + sourceSelectorPanel.revalAndRepaint(); + + telemetryPanel.getTelemetryLabel().setFont(font); + telemetryPanel.revalAndRepaint(); + + rightContainer.revalidate(); + rightContainer.repaint(); + } + }); + + // stop color-picking mode when changing pipeline + // TODO: find out why this breaks everything????? + // eocvSim.pipelineManager.onPipelineChange.doPersistent(() -> colorPicker.stopPicking()); + } + + public boolean hasFinishedInit() { return hasFinishedInitializing; } + + public void waitForFinishingInit() { + while (!hasFinishedInitializing) { + Thread.yield(); + } + } + + public void close() { + SwingUtilities.invokeLater(() -> { + frame.setVisible(false); + viewport.stop(); + + //close all asyncpleasewait dialogs + for (AsyncPleaseWaitDialog dialog : pleaseWaitDialogs) { + if (dialog != null) { + dialog.destroyDialog(); + } + } + + pleaseWaitDialogs.clear(); + + //close all opened frames + for (JFrame frame : childFrames) { + if (frame != null && frame.isVisible()) { + frame.setVisible(false); + frame.dispose(); + } + } + + childFrames.clear(); + + //close all opened dialogs + for (JDialog dialog : childDialogs) { + if (dialog != null && dialog.isVisible()) { + dialog.setVisible(false); + dialog.dispose(); + } + } + + childDialogs.clear(); + frame.dispose(); + viewport.flush(); + }); + } + + private void setFrameTitle(String title, String titleMsg) { + frame.setTitle(title + " - " + titleMsg); + } + + public void setTitle(String title) { + this.title = title; + if (!beforeTitle.equals(title)) setFrameTitle(title, titleMsg); + beforeTitle = title; + } + + public void setTitleMessage(String titleMsg) { + this.titleMsg = titleMsg; + if (!beforeTitleMsg.equals(title)) setFrameTitle(title, titleMsg); + beforeTitleMsg = titleMsg; + } + + public void updateTunerFields(List fields) { + tunerMenuPanel.removeAll(); + tunerMenuPanel.repaint(); + + for (TunableFieldPanel fieldPanel : fields) { + tunerMenuPanel.add(fieldPanel); + fieldPanel.showFieldPanel(); + } + } + + public void asyncCompilePipelines() { + if(PipelineCompiler.Companion.getIS_USABLE()) { + menuBar.workspCompile.setEnabled(false); + pipelineSelectorPanel.getButtonsPanel().getPipelineCompileBtt().setEnabled(false); + + eocvSim.pipelineManager.compiledPipelineManager.asyncCompile(true, (result) -> { + menuBar.workspCompile.setEnabled(true); + pipelineSelectorPanel.getButtonsPanel().getPipelineCompileBtt().setEnabled(true); + + return Unit.INSTANCE; + }); + } else { + compilingUnsupported(); + } + } + + public void compilingUnsupported() { + asyncPleaseWaitDialog( + "Runtime compiling is not supported on this JVM", + "For further info, check the EOCV-Sim GitHub repo", + "Close", + new Dimension(320, 160), + true, true + ); + } + + public void selectPipelinesWorkspace() { + DialogFactory.createFileChooser( + frame, DialogFactory.FileChooser.Mode.DIRECTORY_SELECT + ).addCloseListener((OPTION, selectedFile, selectedFileFilter) -> { + if (OPTION == JFileChooser.APPROVE_OPTION) { + if(!selectedFile.exists()) selectedFile.mkdir(); + + eocvSim.onMainUpdate.doOnce(() -> + eocvSim.workspaceManager.setWorkspaceFile(selectedFile) + ); + } + }); + } + + public void createVSCodeWorkspace() { + DialogFactory.createFileChooser(frame, DialogFactory.FileChooser.Mode.DIRECTORY_SELECT) + .addCloseListener((OPTION, selectedFile, selectedFileFilter) -> { + if(OPTION == JFileChooser.APPROVE_OPTION) { + if(!selectedFile.exists()) selectedFile.mkdir(); + + if(selectedFile.isDirectory() && + Objects.requireNonNull(selectedFile.listFiles()).length == 0) { + eocvSim.workspaceManager.createWorkspaceWithTemplateAsync(selectedFile, GradleWorkspaceTemplate.INSTANCE); + } else { + asyncPleaseWaitDialog( + "The selected directory must be empty", "Select an empty directory or create a new one", + "Retry", new Dimension(320, 160), true, true + ).onCancel(this::createVSCodeWorkspace); + } + } + }); + } + + // PLEASE WAIT DIALOGS + + public boolean pleaseWaitDialog(JDialog diag, String message, String subMessage, String cancelBttText, Dimension size, boolean cancellable, AsyncPleaseWaitDialog apwd, boolean isError) { + final JDialog dialog = diag == null ? new JDialog(this.frame) : diag; + + boolean addSubMessage = subMessage != null; + + int rows = 3; + if (!addSubMessage) { + rows--; + } + + dialog.setModal(true); + dialog.setLayout(new GridLayout(rows, 1)); + + if (isError) { + dialog.setTitle("Operation failed"); + } else { + dialog.setTitle("Operation in progress"); + } + + JLabel msg = new JLabel(message); + msg.setHorizontalAlignment(JLabel.CENTER); + msg.setVerticalAlignment(JLabel.CENTER); + + dialog.add(msg); + + JLabel subMsg = null; + if (addSubMessage) { + + subMsg = new JLabel(subMessage); + subMsg.setHorizontalAlignment(JLabel.CENTER); + subMsg.setVerticalAlignment(JLabel.CENTER); + + dialog.add(subMsg); + + } + + JPanel exitBttPanel = new JPanel(new FlowLayout()); + JButton cancelBtt = new JButton(cancelBttText); + + cancelBtt.setEnabled(cancellable); + + exitBttPanel.add(cancelBtt); + + boolean[] cancelled = {false}; + + cancelBtt.addActionListener(e -> { + cancelled[0] = true; + dialog.setVisible(false); + dialog.dispose(); + }); + + dialog.add(exitBttPanel); + + if (apwd != null) { + apwd.msg = msg; + apwd.subMsg = subMsg; + apwd.cancelBtt = cancelBtt; + } + + if(size == null) size = new Dimension(400, 200); + dialog.setSize(size); + + dialog.setLocationRelativeTo(null); + dialog.setResizable(false); + dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); + + dialog.setVisible(true); + + return cancelled[0]; + } + + public void pleaseWaitDialog(JDialog dialog, String message, String subMessage, String cancelBttText, Dimension size, boolean cancellable) { + pleaseWaitDialog(dialog, message, subMessage, cancelBttText, size, cancellable, null, false); + } + + public void pleaseWaitDialog(String message, String subMessage, String cancelBttText, Dimension size, boolean cancellable) { + pleaseWaitDialog(null, message, subMessage, cancelBttText, size, cancellable, null, false); + } + + public AsyncPleaseWaitDialog asyncPleaseWaitDialog(String message, String subMessage, String cancelBttText, Dimension size, boolean cancellable, boolean isError) { + AsyncPleaseWaitDialog rPWD = new AsyncPleaseWaitDialog(message, subMessage, cancelBttText, size, cancellable, isError, eocvSim); + SwingUtilities.invokeLater(rPWD); + + return rPWD; + } + + public AsyncPleaseWaitDialog asyncPleaseWaitDialog(String message, String subMessage, String cancelBttText, Dimension size, boolean cancellable) { + AsyncPleaseWaitDialog rPWD = new AsyncPleaseWaitDialog(message, subMessage, cancelBttText, size, cancellable, false, eocvSim); + SwingUtilities.invokeLater(rPWD); + + return rPWD; + } + + public class AsyncPleaseWaitDialog implements Runnable { + + public volatile JDialog dialog = new JDialog(frame); + + public volatile JLabel msg = null; + public volatile JLabel subMsg = null; + + public volatile JButton cancelBtt = null; + + public volatile boolean wasCancelled = false; + public volatile boolean isError; + + public volatile String initialMessage; + public volatile String initialSubMessage; + + public volatile boolean isDestroyed = false; + + String message; + String subMessage; + String cancelBttText; + + Dimension size; + + boolean cancellable; + + private final ArrayList onCancelRunnables = new ArrayList<>(); + + public AsyncPleaseWaitDialog(String message, String subMessage, String cancelBttText, Dimension size, boolean cancellable, boolean isError, EOCVSim eocvSim) { + this.message = message; + this.subMessage = subMessage; + this.initialMessage = message; + this.initialSubMessage = subMessage; + this.cancelBttText = cancelBttText; + + this.size = size; + this.cancellable = cancellable; + + this.isError = isError; + + eocvSim.visualizer.pleaseWaitDialogs.add(this); + } + + public void onCancel(Runnable runn) { + onCancelRunnables.add(runn); + } + + @Override + public void run() { + wasCancelled = pleaseWaitDialog(dialog, message, subMessage, cancelBttText, size, cancellable, this, isError); + + if (wasCancelled) { + for (Runnable runn : onCancelRunnables) { + runn.run(); + } + } + } + + public void destroyDialog() { + if (!isDestroyed) { + dialog.setVisible(false); + dialog.dispose(); + isDestroyed = true; + } + } + + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/ImageX.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/ImageX.java index 52c29319..7a79b711 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/ImageX.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/ImageX.java @@ -1,88 +1,88 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.component; - -import com.github.serivesmejia.eocvsim.gui.util.GuiUtil; -import com.github.serivesmejia.eocvsim.util.CvUtil; -import org.opencv.core.Mat; - -import javax.swing.*; -import java.awt.*; -import java.awt.image.BufferedImage; - -public class ImageX extends JLabel { - - volatile ImageIcon icon; - - public ImageX() { - super(); - } - - public ImageX(ImageIcon img) { - this(); - setImage(img); - } - - public ImageX(BufferedImage img) { - this(); - setImage(img); - } - - public void setImage(ImageIcon img) { - if (icon != null) - icon.getImage().flush(); //flush old image :p - - icon = img; - - setIcon(icon); //set to the new image - } - - public synchronized void setImage(BufferedImage img) { - Graphics2D g2d = (Graphics2D) getGraphics(); - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - setImage(new ImageIcon(img)); //set to the new image - } - - public synchronized void setImageMat(Mat m) { - setImage(CvUtil.matToBufferedImage(m)); - } - - public synchronized BufferedImage getImage() { - return (BufferedImage)icon.getImage(); - } - - @Override - public void setSize(int width, int height) { - super.setSize(width, height); - setImage(GuiUtil.scaleImage(icon, width, height)); //set to the new image - } - - @Override - public void setSize(Dimension dimension) { - super.setSize(dimension); - setImage(GuiUtil.scaleImage(icon, (int)dimension.getWidth(), (int)dimension.getHeight())); //set to the new image - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.component; + +import com.github.serivesmejia.eocvsim.gui.util.GuiUtil; +import com.github.serivesmejia.eocvsim.util.CvUtil; +import org.opencv.core.Mat; + +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; + +public class ImageX extends JLabel { + + volatile ImageIcon icon; + + public ImageX() { + super(); + } + + public ImageX(ImageIcon img) { + this(); + setImage(img); + } + + public ImageX(BufferedImage img) { + this(); + setImage(img); + } + + public void setImage(ImageIcon img) { + if (icon != null) + icon.getImage().flush(); //flush old image :p + + icon = img; + + setIcon(icon); //set to the new image + } + + public synchronized void setImage(BufferedImage img) { + Graphics2D g2d = (Graphics2D) getGraphics(); + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + setImage(new ImageIcon(img)); //set to the new image + } + + public synchronized void setImageMat(Mat m) { + setImage(CvUtil.matToBufferedImage(m)); + } + + public synchronized BufferedImage getImage() { + return (BufferedImage)icon.getImage(); + } + + @Override + public void setSize(int width, int height) { + super.setSize(width, height); + setImage(GuiUtil.scaleImage(icon, width, height)); //set to the new image + } + + @Override + public void setSize(Dimension dimension) { + super.setSize(dimension); + setImage(GuiUtil.scaleImage(icon, (int)dimension.getWidth(), (int)dimension.getHeight())); //set to the new image + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/PopupX.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/PopupX.kt index 848954d5..15cf5bd3 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/PopupX.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/PopupX.kt @@ -1,100 +1,100 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.component - -import com.github.serivesmejia.eocvsim.util.event.EventHandler -import java.awt.Window -import java.awt.event.KeyAdapter -import java.awt.event.KeyEvent -import java.awt.event.WindowEvent -import java.awt.event.WindowFocusListener -import javax.swing.JPanel -import javax.swing.JPopupMenu -import javax.swing.JWindow -import javax.swing.Popup - -class PopupX @JvmOverloads constructor(windowAncestor: Window, - private val panel: JPanel, - private var x: Int, - private var y: Int, - var closeOnFocusLost: Boolean = true, - private val fixX: Boolean = false, - private val fixY: Boolean = true) : Popup(), WindowFocusListener { - - val window = JWindow(windowAncestor) - - @JvmField val onShow = EventHandler("PopupX-OnShow") - @JvmField val onHide = EventHandler("PopupX-OnHide") - - init { - window.isFocusable = true - window.setLocation(x, y) - window.contentPane = panel - - panel.border = JPopupMenu().border - - window.size = panel.preferredSize - - windowAncestor.addKeyListener(object: KeyAdapter() { - override fun keyPressed(e: KeyEvent?) { - if(e?.keyCode == KeyEvent.VK_ESCAPE) { - hide() - windowAncestor.removeKeyListener(this) - } - } - }) - } - - override fun show() { - window.addWindowFocusListener(this) - window.isVisible = true - - //fixes position since our panel dimensions - //aren't known until it's set visible (above) - if(fixX) x -= panel.width / 4 - if(fixY) y -= panel.height - setLocation(x, y) - - onShow.run() - } - - override fun hide() { - if(!window.isVisible) return - - window.removeWindowFocusListener(this) - window.isVisible = false - onHide.run() - } - - override fun windowGainedFocus(e: WindowEvent?) {} - - override fun windowLostFocus(e: WindowEvent?) { - if(closeOnFocusLost) { - hide() - } - } - - fun setLocation(width: Int, height: Int) = window.setLocation(width, height) - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.component + +import com.github.serivesmejia.eocvsim.util.event.EventHandler +import java.awt.Window +import java.awt.event.KeyAdapter +import java.awt.event.KeyEvent +import java.awt.event.WindowEvent +import java.awt.event.WindowFocusListener +import javax.swing.JPanel +import javax.swing.JPopupMenu +import javax.swing.JWindow +import javax.swing.Popup + +class PopupX @JvmOverloads constructor(windowAncestor: Window, + private val panel: JPanel, + private var x: Int, + private var y: Int, + var closeOnFocusLost: Boolean = true, + private val fixX: Boolean = false, + private val fixY: Boolean = true) : Popup(), WindowFocusListener { + + val window = JWindow(windowAncestor) + + @JvmField val onShow = EventHandler("PopupX-OnShow") + @JvmField val onHide = EventHandler("PopupX-OnHide") + + init { + window.isFocusable = true + window.setLocation(x, y) + window.contentPane = panel + + panel.border = JPopupMenu().border + + window.size = panel.preferredSize + + windowAncestor.addKeyListener(object: KeyAdapter() { + override fun keyPressed(e: KeyEvent?) { + if(e?.keyCode == KeyEvent.VK_ESCAPE) { + hide() + windowAncestor.removeKeyListener(this) + } + } + }) + } + + override fun show() { + window.addWindowFocusListener(this) + window.isVisible = true + + //fixes position since our panel dimensions + //aren't known until it's set visible (above) + if(fixX) x -= panel.width / 4 + if(fixY) y -= panel.height + setLocation(x, y) + + onShow.run() + } + + override fun hide() { + if(!window.isVisible) return + + window.removeWindowFocusListener(this) + window.isVisible = false + onHide.run() + } + + override fun windowGainedFocus(e: WindowEvent?) {} + + override fun windowLostFocus(e: WindowEvent?) { + if(closeOnFocusLost) { + hide() + } + } + + fun setLocation(width: Int, height: Int) = window.setLocation(width, height) + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/SliderX.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/SliderX.kt index 98d617f0..bfb5e775 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/SliderX.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/SliderX.kt @@ -1,72 +1,72 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.component - -import com.qualcomm.robotcore.util.Range -import javax.swing.JSlider -import kotlin.math.roundToInt - -/** - * Allows for a slider to take a range of type double - * and return a value of type double, instead of int. - * - * Achieved by upscaling the input bounds and the input - * value by a certain amount (multiplier of 10), and - * downscaling the value when getting it - */ -open class SliderX(private var minBound: Double, - private var maxBound: Double, - private val scale: Int) : JSlider() { - - var scaledValue: Double = 0.0 - set(value) { - field = Range.clip(value * scale, minimum.toDouble(), maximum.toDouble()) - this.value = field.roundToInt() - } - get() { - return Range.clip(this.value.toDouble() / scale, minBound, maxBound) - } - - init { - setScaledBounds(minBound, maxBound) - setMajorTickSpacing(scale) - setMinorTickSpacing(scale / 4) - } - - fun setScaledBounds(minBound: Double, maxBound: Double) { - //for some reason we have to scale min bound when - //going negative... but not when going positive - this.minBound = if(minBound > 0) { - minBound - } else { - minBound * scale - } - - this.maxBound = maxBound * scale - - minimum = this.minBound.roundToInt() - maximum = this.maxBound.roundToInt() - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.component + +import com.qualcomm.robotcore.util.Range +import javax.swing.JSlider +import kotlin.math.roundToInt + +/** + * Allows for a slider to take a range of type double + * and return a value of type double, instead of int. + * + * Achieved by upscaling the input bounds and the input + * value by a certain amount (multiplier of 10), and + * downscaling the value when getting it + */ +open class SliderX(private var minBound: Double, + private var maxBound: Double, + private val scale: Int) : JSlider() { + + var scaledValue: Double = 0.0 + set(value) { + field = Range.clip(value * scale, minimum.toDouble(), maximum.toDouble()) + this.value = field.roundToInt() + } + get() { + return Range.clip(this.value.toDouble() / scale, minBound, maxBound) + } + + init { + setScaledBounds(minBound, maxBound) + setMajorTickSpacing(scale) + setMinorTickSpacing(scale / 4) + } + + fun setScaledBounds(minBound: Double, maxBound: Double) { + //for some reason we have to scale min bound when + //going negative... but not when going positive + this.minBound = if(minBound > 0) { + minBound + } else { + minBound * scale + } + + this.maxBound = maxBound * scale + + minimum = this.minBound.roundToInt() + maximum = this.maxBound.roundToInt() + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/Viewport.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/Viewport.java index 33349f1d..a5f25c10 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/Viewport.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/Viewport.java @@ -1,144 +1,144 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.component; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.config.Config; -import com.github.serivesmejia.eocvsim.gui.util.MatPoster; -import com.github.serivesmejia.eocvsim.util.image.DynamicBufferedImageRecycler; -import com.github.serivesmejia.eocvsim.util.CvUtil; -import com.github.serivesmejia.eocvsim.util.Log; -import com.qualcomm.robotcore.util.Range; -import org.opencv.core.Mat; -import org.opencv.core.Size; -import org.opencv.imgproc.Imgproc; - -import javax.swing.*; -import java.awt.*; -import java.awt.image.BufferedImage; - -public class Viewport extends JPanel { - - public final ImageX image = new ImageX(); - public final MatPoster matPoster; - - private Mat lastVisualizedMat = null; - private Mat lastVisualizedScaledMat = null; - - private final DynamicBufferedImageRecycler buffImgGiver = new DynamicBufferedImageRecycler(); - - private volatile BufferedImage lastBuffImage; - private volatile Dimension lastDimension; - - private double scale; - - private final EOCVSim eocvSim; - - public Viewport(EOCVSim eocvSim, int maxQueueItems) { - super(new GridBagLayout()); - - this.eocvSim = eocvSim; - setViewportScale(eocvSim.configManager.getConfig().zoom); - - add(image, new GridBagConstraints()); - - matPoster = new MatPoster("Viewport", maxQueueItems); - attachToPoster(matPoster); - } - - public void postMatAsync(Mat mat) { - matPoster.post(mat); - } - - public synchronized void postMat(Mat mat) { - if(lastVisualizedMat == null) lastVisualizedMat = new Mat(); //create latest mat if we have null reference - if(lastVisualizedScaledMat == null) lastVisualizedScaledMat = new Mat(); //create last scaled mat if null reference - - JFrame frame = (JFrame) SwingUtilities.getWindowAncestor(this); - - mat.copyTo(lastVisualizedMat); //copy given mat to viewport latest one - - double wScale = (double) frame.getWidth() / mat.width(); - double hScale = (double) frame.getHeight() / mat.height(); - - double calcScale = (wScale / hScale) * 1.5; - double finalScale = Math.max(0.1, Math.min(3, scale * calcScale)); - - Size size = new Size(mat.width() * finalScale, mat.height() * finalScale); - Imgproc.resize(mat, lastVisualizedScaledMat, size, 0.0, 0.0, Imgproc.INTER_AREA); //resize mat to lastVisualizedScaledMat - - Dimension newDimension = new Dimension(lastVisualizedScaledMat.width(), lastVisualizedScaledMat.height()); - - if(lastBuffImage != null) buffImgGiver.returnBufferedImage(lastBuffImage); - - lastBuffImage = buffImgGiver.giveBufferedImage(newDimension, 2); - lastDimension = newDimension; - - CvUtil.matToBufferedImage(lastVisualizedScaledMat, lastBuffImage); - - image.setImage(lastBuffImage); //set buff image to ImageX component - - eocvSim.configManager.getConfig().zoom = scale; //store latest scale if store setting turned on - } - - public void attachToPoster(MatPoster poster) { - poster.addPostable((m) -> { - try { - Imgproc.cvtColor(m, m, Imgproc.COLOR_RGB2BGR); - postMat(m); - } catch(Exception ex) { - Log.error("Viewport-Postable", "Couldn't visualize last mat", ex); - } - }); - } - - public void flush() { - buffImgGiver.flushAll(); - } - - public void stop() { - matPoster.stop(); - flush(); - } - - public synchronized void setViewportScale(double scale) { - scale = Range.clip(scale, 0.1, 3); - - boolean scaleChanged = this.scale != scale; - this.scale = scale; - - if(lastVisualizedMat != null && scaleChanged) - postMat(lastVisualizedMat); - - } - - public synchronized Mat getLastVisualizedMat() { - return lastVisualizedMat; - } - - public synchronized double getViewportScale() { - return scale; - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.component; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.config.Config; +import com.github.serivesmejia.eocvsim.gui.util.MatPoster; +import com.github.serivesmejia.eocvsim.util.image.DynamicBufferedImageRecycler; +import com.github.serivesmejia.eocvsim.util.CvUtil; +import com.github.serivesmejia.eocvsim.util.Log; +import com.qualcomm.robotcore.util.Range; +import org.opencv.core.Mat; +import org.opencv.core.Size; +import org.opencv.imgproc.Imgproc; + +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; + +public class Viewport extends JPanel { + + public final ImageX image = new ImageX(); + public final MatPoster matPoster; + + private Mat lastVisualizedMat = null; + private Mat lastVisualizedScaledMat = null; + + private final DynamicBufferedImageRecycler buffImgGiver = new DynamicBufferedImageRecycler(); + + private volatile BufferedImage lastBuffImage; + private volatile Dimension lastDimension; + + private double scale; + + private final EOCVSim eocvSim; + + public Viewport(EOCVSim eocvSim, int maxQueueItems) { + super(new GridBagLayout()); + + this.eocvSim = eocvSim; + setViewportScale(eocvSim.configManager.getConfig().zoom); + + add(image, new GridBagConstraints()); + + matPoster = new MatPoster("Viewport", maxQueueItems); + attachToPoster(matPoster); + } + + public void postMatAsync(Mat mat) { + matPoster.post(mat); + } + + public synchronized void postMat(Mat mat) { + if(lastVisualizedMat == null) lastVisualizedMat = new Mat(); //create latest mat if we have null reference + if(lastVisualizedScaledMat == null) lastVisualizedScaledMat = new Mat(); //create last scaled mat if null reference + + JFrame frame = (JFrame) SwingUtilities.getWindowAncestor(this); + + mat.copyTo(lastVisualizedMat); //copy given mat to viewport latest one + + double wScale = (double) frame.getWidth() / mat.width(); + double hScale = (double) frame.getHeight() / mat.height(); + + double calcScale = (wScale / hScale) * 1.5; + double finalScale = Math.max(0.1, Math.min(3, scale * calcScale)); + + Size size = new Size(mat.width() * finalScale, mat.height() * finalScale); + Imgproc.resize(mat, lastVisualizedScaledMat, size, 0.0, 0.0, Imgproc.INTER_AREA); //resize mat to lastVisualizedScaledMat + + Dimension newDimension = new Dimension(lastVisualizedScaledMat.width(), lastVisualizedScaledMat.height()); + + if(lastBuffImage != null) buffImgGiver.returnBufferedImage(lastBuffImage); + + lastBuffImage = buffImgGiver.giveBufferedImage(newDimension, 2); + lastDimension = newDimension; + + CvUtil.matToBufferedImage(lastVisualizedScaledMat, lastBuffImage); + + image.setImage(lastBuffImage); //set buff image to ImageX component + + eocvSim.configManager.getConfig().zoom = scale; //store latest scale if store setting turned on + } + + public void attachToPoster(MatPoster poster) { + poster.addPostable((m) -> { + try { + Imgproc.cvtColor(m, m, Imgproc.COLOR_RGB2BGR); + postMat(m); + } catch(Exception ex) { + Log.error("Viewport-Postable", "Couldn't visualize last mat", ex); + } + }); + } + + public void flush() { + buffImgGiver.flushAll(); + } + + public void stop() { + matPoster.stop(); + flush(); + } + + public synchronized void setViewportScale(double scale) { + scale = Range.clip(scale, 0.1, 3); + + boolean scaleChanged = this.scale != scale; + this.scale = scale; + + if(lastVisualizedMat != null && scaleChanged) + postMat(lastVisualizedMat); + + } + + public synchronized Mat getLastVisualizedMat() { + return lastVisualizedMat; + } + + public synchronized double getViewportScale() { + return scale; + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/input/EnumComboBox.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/input/EnumComboBox.kt index a06bd938..3530403c 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/input/EnumComboBox.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/input/EnumComboBox.kt @@ -1,78 +1,78 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.component.input - -import com.github.serivesmejia.eocvsim.util.event.EventHandler -import javax.swing.JComboBox -import javax.swing.JLabel -import javax.swing.JPanel - -class EnumComboBox> @JvmOverloads constructor( - descriptiveText: String = "Select a value:", - private val clazz: Class, - values: Array, - private val nameSupplier: (T) -> String = { it.name }, - private val enumSupplier: (String) -> T = { - java.lang.Enum.valueOf(clazz, it) as T - } -) : JPanel() { - - val descriptiveLabel = JLabel(descriptiveText) - val comboBox = JComboBox() - - var selectedEnum: T? - set(value) { - value?.let { - comboBox.selectedItem = nameSupplier(it) - } - } - get() { - comboBox.selectedItem?.let { - return enumSupplier(comboBox.selectedItem!!.toString()) - } - return null - } - - val onSelect = EventHandler("EnumComboBox-OnSelect") - - init { - if(descriptiveText.trim() != "") { - descriptiveLabel.horizontalAlignment = JLabel.LEFT - add(descriptiveLabel) - } - - for(value in values) { - comboBox.addItem(nameSupplier(value)) - } - add(comboBox) - - comboBox.addActionListener { onSelect.run() } - } - - - fun removeEnumOption(enum: T) { - comboBox.removeItem(nameSupplier(enum)) - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.component.input + +import com.github.serivesmejia.eocvsim.util.event.EventHandler +import javax.swing.JComboBox +import javax.swing.JLabel +import javax.swing.JPanel + +class EnumComboBox> @JvmOverloads constructor( + descriptiveText: String = "Select a value:", + private val clazz: Class, + values: Array, + private val nameSupplier: (T) -> String = { it.name }, + private val enumSupplier: (String) -> T = { + java.lang.Enum.valueOf(clazz, it) as T + } +) : JPanel() { + + val descriptiveLabel = JLabel(descriptiveText) + val comboBox = JComboBox() + + var selectedEnum: T? + set(value) { + value?.let { + comboBox.selectedItem = nameSupplier(it) + } + } + get() { + comboBox.selectedItem?.let { + return enumSupplier(comboBox.selectedItem!!.toString()) + } + return null + } + + val onSelect = EventHandler("EnumComboBox-OnSelect") + + init { + if(descriptiveText.trim() != "") { + descriptiveLabel.horizontalAlignment = JLabel.LEFT + add(descriptiveLabel) + } + + for(value in values) { + comboBox.addItem(nameSupplier(value)) + } + add(comboBox) + + comboBox.addActionListener { onSelect.run() } + } + + + fun removeEnumOption(enum: T) { + comboBox.removeItem(nameSupplier(enum)) + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/input/FileSelector.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/input/FileSelector.kt index 73340b65..16a47144 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/input/FileSelector.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/input/FileSelector.kt @@ -1,75 +1,75 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.component.input - -import com.github.serivesmejia.eocvsim.gui.DialogFactory -import com.github.serivesmejia.eocvsim.util.event.EventHandler -import java.awt.FlowLayout -import java.io.File -import javax.swing.* -import javax.swing.filechooser.FileFilter - -class FileSelector(columns: Int = 18, - mode: DialogFactory.FileChooser.Mode, - vararg fileFilters: FileFilter?) : JPanel(FlowLayout()) { - - constructor(columns: Int) : this(columns, DialogFactory.FileChooser.Mode.FILE_SELECT) - - constructor(columns: Int, vararg fileFilters: FileFilter?) : this(columns, DialogFactory.FileChooser.Mode.FILE_SELECT, *fileFilters) - - constructor(columns: Int, mode: DialogFactory.FileChooser.Mode) : this(columns, mode, null) - - @JvmField val onFileSelect = EventHandler("OnFileSelect") - - val dirTextField = JTextField(columns) - val selectDirButton = JButton("Select file...") - - var lastSelectedFile: File? = null - set(value) { - dirTextField.text = value?.absolutePath ?: "" - field = value - onFileSelect.run() - } - - var lastSelectedFileFilter: FileFilter? = null - private set - - init { - dirTextField.isEditable = false - - selectDirButton.addActionListener { - val frame = SwingUtilities.getWindowAncestor(this) - DialogFactory.createFileChooser(frame, mode, *fileFilters).addCloseListener { returnVal: Int, selectedFile: File?, selectedFileFilter: FileFilter? -> - if (returnVal == JFileChooser.APPROVE_OPTION) { - lastSelectedFileFilter = selectedFileFilter - lastSelectedFile = selectedFile - } - } - } - - add(dirTextField) - add(selectDirButton) - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.component.input + +import com.github.serivesmejia.eocvsim.gui.DialogFactory +import com.github.serivesmejia.eocvsim.util.event.EventHandler +import java.awt.FlowLayout +import java.io.File +import javax.swing.* +import javax.swing.filechooser.FileFilter + +class FileSelector(columns: Int = 18, + mode: DialogFactory.FileChooser.Mode, + vararg fileFilters: FileFilter?) : JPanel(FlowLayout()) { + + constructor(columns: Int) : this(columns, DialogFactory.FileChooser.Mode.FILE_SELECT) + + constructor(columns: Int, vararg fileFilters: FileFilter?) : this(columns, DialogFactory.FileChooser.Mode.FILE_SELECT, *fileFilters) + + constructor(columns: Int, mode: DialogFactory.FileChooser.Mode) : this(columns, mode, null) + + @JvmField val onFileSelect = EventHandler("OnFileSelect") + + val dirTextField = JTextField(columns) + val selectDirButton = JButton("Select file...") + + var lastSelectedFile: File? = null + set(value) { + dirTextField.text = value?.absolutePath ?: "" + field = value + onFileSelect.run() + } + + var lastSelectedFileFilter: FileFilter? = null + private set + + init { + dirTextField.isEditable = false + + selectDirButton.addActionListener { + val frame = SwingUtilities.getWindowAncestor(this) + DialogFactory.createFileChooser(frame, mode, *fileFilters).addCloseListener { returnVal: Int, selectedFile: File?, selectedFileFilter: FileFilter? -> + if (returnVal == JFileChooser.APPROVE_OPTION) { + lastSelectedFileFilter = selectedFileFilter + lastSelectedFile = selectedFile + } + } + } + + add(dirTextField) + add(selectDirButton) + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/input/SizeFields.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/input/SizeFields.kt index d2500b30..a3fc9faa 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/input/SizeFields.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/input/SizeFields.kt @@ -1,141 +1,141 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.component.input - -import com.github.serivesmejia.eocvsim.EOCVSim -import com.github.serivesmejia.eocvsim.gui.util.ValidCharactersDocumentFilter -import com.github.serivesmejia.eocvsim.gui.util.extension.SwingExt.documentFilter -import com.github.serivesmejia.eocvsim.util.event.EventHandler -import org.opencv.core.Size -import java.awt.Color -import java.awt.FlowLayout -import java.util.* -import javax.swing.JLabel -import javax.swing.JPanel -import javax.swing.JTextField -import javax.swing.border.LineBorder -import javax.swing.event.DocumentEvent -import javax.swing.event.DocumentListener -import kotlin.math.roundToInt - -class SizeFields(initialSize: Size = EOCVSim.DEFAULT_EOCV_SIZE, - allowDecimalValues: Boolean = false, - allowNegativeValues: Boolean = false, - descriptiveText: String = "Size: ", - middleText: String = " x ") : JPanel(FlowLayout()) { - - constructor(initialSize: Size, allowDecimalValues: Boolean, descriptiveText: String) - : this(initialSize, allowDecimalValues, false, descriptiveText, " x ") - - val widthTextField = JTextField(4) - val heightTextField = JTextField(4) - - private val widthValidator: ValidCharactersDocumentFilter - private val heightValidator: ValidCharactersDocumentFilter - - @get:Synchronized - val lastValidWidth: Double - get() = widthValidator.lastValid - - @get:Synchronized - val currentWidth: Double - get() = widthTextField.text.toDouble() - - @get:Synchronized - val lastValidHeight: Double - get() = heightValidator.lastValid - - @get:Synchronized - val currentHeight: Double - get() = heightTextField.text.toDouble() - - @get:Synchronized - val lastValidSize: Size - get() = Size(lastValidWidth, lastValidHeight) - - @get:Synchronized - val currentSize: Size - get() = Size(currentWidth, currentHeight) - - private val validChars = ArrayList() - - val valid: Boolean - get() = widthValidator.valid && heightValidator.valid && widthTextField.text != "" && heightTextField.text != "" - - @JvmField val onChange = EventHandler("SizeFields-OnChange") - - init { - //add all valid characters for non decimal numeric fields - Collections.addAll(validChars, '0', '1', '2', '3', '4', '5', '6', '7', '8', '9') - if(allowDecimalValues) { - validChars.add('.') - } - if(allowNegativeValues) { - validChars.add('-') - } - - widthValidator = ValidCharactersDocumentFilter(validChars.toTypedArray()) - heightValidator = ValidCharactersDocumentFilter(validChars.toTypedArray()) - - widthTextField.documentFilter = widthValidator - widthTextField.document.addDocumentListener(BorderChangerListener(widthTextField, widthValidator, onChange)) - widthTextField.text = "${ if(allowDecimalValues) { initialSize.width } else { initialSize.width.roundToInt() } }" - - heightTextField.documentFilter = heightValidator - heightTextField.document.addDocumentListener(BorderChangerListener(heightTextField, heightValidator, onChange)) - heightTextField.text = "${ if(allowDecimalValues) { initialSize.height } else { initialSize.height.roundToInt() } }" - - val sizeLabel = JLabel(descriptiveText) - sizeLabel.horizontalAlignment = JLabel.LEFT - add(sizeLabel) - - add(widthTextField) - - val xLabel = JLabel(middleText) - xLabel.horizontalAlignment = JLabel.CENTER - add(xLabel) - - add(heightTextField) - } - - private class BorderChangerListener(val field: JTextField, val validator: ValidCharactersDocumentFilter, val onChange: EventHandler? = null): DocumentListener { - - val initialBorder = field.border - val redBorder = LineBorder(Color(255, 79, 79), 2) - - override fun insertUpdate(e: DocumentEvent?) = change() - override fun removeUpdate(e: DocumentEvent?) = change() - override fun changedUpdate(e: DocumentEvent?) = change() - fun change() { - if(validator.valid && field.text != "") { - field.border = initialBorder - } else { - field.border = redBorder - } - - onChange?.run() - } - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.component.input + +import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.gui.util.ValidCharactersDocumentFilter +import com.github.serivesmejia.eocvsim.gui.util.extension.SwingExt.documentFilter +import com.github.serivesmejia.eocvsim.util.event.EventHandler +import org.opencv.core.Size +import java.awt.Color +import java.awt.FlowLayout +import java.util.* +import javax.swing.JLabel +import javax.swing.JPanel +import javax.swing.JTextField +import javax.swing.border.LineBorder +import javax.swing.event.DocumentEvent +import javax.swing.event.DocumentListener +import kotlin.math.roundToInt + +class SizeFields(initialSize: Size = EOCVSim.DEFAULT_EOCV_SIZE, + allowDecimalValues: Boolean = false, + allowNegativeValues: Boolean = false, + descriptiveText: String = "Size: ", + middleText: String = " x ") : JPanel(FlowLayout()) { + + constructor(initialSize: Size, allowDecimalValues: Boolean, descriptiveText: String) + : this(initialSize, allowDecimalValues, false, descriptiveText, " x ") + + val widthTextField = JTextField(4) + val heightTextField = JTextField(4) + + private val widthValidator: ValidCharactersDocumentFilter + private val heightValidator: ValidCharactersDocumentFilter + + @get:Synchronized + val lastValidWidth: Double + get() = widthValidator.lastValid + + @get:Synchronized + val currentWidth: Double + get() = widthTextField.text.toDouble() + + @get:Synchronized + val lastValidHeight: Double + get() = heightValidator.lastValid + + @get:Synchronized + val currentHeight: Double + get() = heightTextField.text.toDouble() + + @get:Synchronized + val lastValidSize: Size + get() = Size(lastValidWidth, lastValidHeight) + + @get:Synchronized + val currentSize: Size + get() = Size(currentWidth, currentHeight) + + private val validChars = ArrayList() + + val valid: Boolean + get() = widthValidator.valid && heightValidator.valid && widthTextField.text != "" && heightTextField.text != "" + + @JvmField val onChange = EventHandler("SizeFields-OnChange") + + init { + //add all valid characters for non decimal numeric fields + Collections.addAll(validChars, '0', '1', '2', '3', '4', '5', '6', '7', '8', '9') + if(allowDecimalValues) { + validChars.add('.') + } + if(allowNegativeValues) { + validChars.add('-') + } + + widthValidator = ValidCharactersDocumentFilter(validChars.toTypedArray()) + heightValidator = ValidCharactersDocumentFilter(validChars.toTypedArray()) + + widthTextField.documentFilter = widthValidator + widthTextField.document.addDocumentListener(BorderChangerListener(widthTextField, widthValidator, onChange)) + widthTextField.text = "${ if(allowDecimalValues) { initialSize.width } else { initialSize.width.roundToInt() } }" + + heightTextField.documentFilter = heightValidator + heightTextField.document.addDocumentListener(BorderChangerListener(heightTextField, heightValidator, onChange)) + heightTextField.text = "${ if(allowDecimalValues) { initialSize.height } else { initialSize.height.roundToInt() } }" + + val sizeLabel = JLabel(descriptiveText) + sizeLabel.horizontalAlignment = JLabel.LEFT + add(sizeLabel) + + add(widthTextField) + + val xLabel = JLabel(middleText) + xLabel.horizontalAlignment = JLabel.CENTER + add(xLabel) + + add(heightTextField) + } + + private class BorderChangerListener(val field: JTextField, val validator: ValidCharactersDocumentFilter, val onChange: EventHandler? = null): DocumentListener { + + val initialBorder = field.border + val redBorder = LineBorder(Color(255, 79, 79), 2) + + override fun insertUpdate(e: DocumentEvent?) = change() + override fun removeUpdate(e: DocumentEvent?) = change() + override fun changedUpdate(e: DocumentEvent?) = change() + fun change() { + if(validator.valid && field.text != "") { + field.border = initialBorder + } else { + field.border = redBorder + } + + onChange?.run() + } + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/ColorPicker.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/ColorPicker.kt index daf3153f..3244f25a 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/ColorPicker.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/ColorPicker.kt @@ -1,115 +1,115 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.component.tuner - -import com.github.serivesmejia.eocvsim.util.SysUtil -import com.github.serivesmejia.eocvsim.gui.Icons -import com.github.serivesmejia.eocvsim.gui.component.ImageX -import com.github.serivesmejia.eocvsim.gui.component.Viewport -import com.github.serivesmejia.eocvsim.util.event.EventHandler -import org.opencv.core.Scalar -import java.awt.Color -import java.awt.Cursor -import java.awt.Point -import java.awt.event.MouseAdapter -import java.awt.event.MouseEvent -import java.awt.Toolkit - -class ColorPicker(private val imageX: ImageX) { - - companion object { - private val size = if(SysUtil.OS == SysUtil.OperatingSystem.WINDOWS) { - 200 - } else { 35 } - - val colorPickIco = Icons.getImageResized("ico_colorpick_pointer", size, size).image - - val colorPickCursor = Toolkit.getDefaultToolkit().createCustomCursor( - colorPickIco, Point(0, 0), "Color Pick Pointer" - ) - } - - var isPicking = false - private set - - var hasPicked = false - private set - - val onPick = EventHandler("ColorPicker-OnPick") - val onCancel = EventHandler("ColorPicker-OnCancel") - - private var initialCursor: Cursor? = null - - var colorRgb = Scalar(0.0, 0.0, 0.0) - private set - - val clickListener = object: MouseAdapter() { - override fun mouseClicked(e: MouseEvent) { - //if clicked with primary button... - if(e.button == MouseEvent.BUTTON1) { - //get the "packed" (in a single int value) color from the image at mouse position's pixel - val packedColor = imageX.image.getRGB(e.x, e.y) - //parse the "packed" color into four separate channels - val color = Color(packedColor, true) - - //wrap Java's color to OpenCV's Scalar since we're EOCV-Sim not JavaCv-Sim right? - colorRgb = Scalar( - color.red.toDouble(), color.green.toDouble(), color.blue.toDouble() - ) - - hasPicked = true - onPick.run() //run all oick listeners - } else { - onCancel.run() - } - - stopPicking() - } - } - - fun startPicking() { - if(isPicking) return - isPicking = true - hasPicked = false - - imageX.addMouseListener(clickListener) - - initialCursor = imageX.cursor - imageX.cursor = colorPickCursor - } - - fun stopPicking() { - if(!isPicking) return - isPicking = false - - if(!hasPicked) { - onPick.removeAllListeners() - onCancel.run() - } - - imageX.removeMouseListener(clickListener) - imageX.cursor = initialCursor - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.component.tuner + +import com.github.serivesmejia.eocvsim.util.SysUtil +import com.github.serivesmejia.eocvsim.gui.Icons +import com.github.serivesmejia.eocvsim.gui.component.ImageX +import com.github.serivesmejia.eocvsim.gui.component.Viewport +import com.github.serivesmejia.eocvsim.util.event.EventHandler +import org.opencv.core.Scalar +import java.awt.Color +import java.awt.Cursor +import java.awt.Point +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import java.awt.Toolkit + +class ColorPicker(private val imageX: ImageX) { + + companion object { + private val size = if(SysUtil.OS == SysUtil.OperatingSystem.WINDOWS) { + 200 + } else { 35 } + + val colorPickIco = Icons.getImageResized("ico_colorpick_pointer", size, size).image + + val colorPickCursor = Toolkit.getDefaultToolkit().createCustomCursor( + colorPickIco, Point(0, 0), "Color Pick Pointer" + ) + } + + var isPicking = false + private set + + var hasPicked = false + private set + + val onPick = EventHandler("ColorPicker-OnPick") + val onCancel = EventHandler("ColorPicker-OnCancel") + + private var initialCursor: Cursor? = null + + var colorRgb = Scalar(0.0, 0.0, 0.0) + private set + + val clickListener = object: MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + //if clicked with primary button... + if(e.button == MouseEvent.BUTTON1) { + //get the "packed" (in a single int value) color from the image at mouse position's pixel + val packedColor = imageX.image.getRGB(e.x, e.y) + //parse the "packed" color into four separate channels + val color = Color(packedColor, true) + + //wrap Java's color to OpenCV's Scalar since we're EOCV-Sim not JavaCv-Sim right? + colorRgb = Scalar( + color.red.toDouble(), color.green.toDouble(), color.blue.toDouble() + ) + + hasPicked = true + onPick.run() //run all oick listeners + } else { + onCancel.run() + } + + stopPicking() + } + } + + fun startPicking() { + if(isPicking) return + isPicking = true + hasPicked = false + + imageX.addMouseListener(clickListener) + + initialCursor = imageX.cursor + imageX.cursor = colorPickCursor + } + + fun stopPicking() { + if(!isPicking) return + isPicking = false + + if(!hasPicked) { + onPick.removeAllListeners() + onCancel.run() + } + + imageX.removeMouseListener(clickListener) + imageX.cursor = initialCursor + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanel.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanel.java index 71efcf45..37b70b63 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanel.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanel.java @@ -1,219 +1,219 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.component.tuner; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.gui.component.tuner.element.TunableComboBox; -import com.github.serivesmejia.eocvsim.gui.component.tuner.element.TunableSlider; -import com.github.serivesmejia.eocvsim.gui.component.tuner.element.TunableTextField; -import com.github.serivesmejia.eocvsim.tuner.TunableField; - -import javax.swing.*; -import javax.swing.border.SoftBevelBorder; -import java.awt.*; - -public class TunableFieldPanel extends JPanel { - - public final TunableField tunableField; - - public TunableTextField[] fields; - public JPanel fieldsPanel; - - public TunableSlider[] sliders; - public JPanel slidersPanel; - - public JComboBox[] comboBoxes; - - public TunableFieldPanelOptions panelOptions = null; - private final EOCVSim eocvSim; - - private Mode mode; - private boolean reevalConfigRequested = false; - - private boolean hasBeenShown = false; - - public enum Mode { TEXTBOXES, SLIDERS } - - public TunableFieldPanel(TunableField tunableField, EOCVSim eocvSim) { - super(); - - this.tunableField = tunableField; - this.eocvSim = eocvSim; - - tunableField.setTunableFieldPanel(this); - - init(); - } - - private void init() { - //nice look - setBorder(new SoftBevelBorder(SoftBevelBorder.RAISED)); - - panelOptions = new TunableFieldPanelOptions(this, eocvSim); - - if(tunableField.getGuiFieldAmount() > 0) { - add(panelOptions); - } - - JLabel fieldNameLabel = new JLabel(); - fieldNameLabel.setText(tunableField.getFieldName()); - - add(fieldNameLabel); - - int fieldAmount = tunableField.getGuiFieldAmount(); - - fields = new TunableTextField[fieldAmount]; - sliders = new TunableSlider[fieldAmount]; - - fieldsPanel = new JPanel(); - slidersPanel = new JPanel(new GridBagLayout()); - - for (int i = 0 ; i < tunableField.getGuiFieldAmount() ; i++) { - //add the tunable field as a field - TunableTextField field = new TunableTextField(i, tunableField, eocvSim); - fields[i] = field; - - field.setEditable(true); - fieldsPanel.add(field); - - //add the tunable field as a slider - JLabel sliderLabel = new JLabel("0"); - TunableSlider slider = new TunableSlider(i, tunableField, eocvSim, sliderLabel); - sliders[i] = slider; - - GridBagConstraints cSlider = new GridBagConstraints(); - cSlider.gridx = 0; - cSlider.gridy = i; - - GridBagConstraints cLabel = new GridBagConstraints(); - cLabel.gridy = 1; - cLabel.gridy = i; - - slidersPanel.add(slider, cSlider); - slidersPanel.add(sliderLabel, cLabel); - } - - setMode(Mode.TEXTBOXES); - - comboBoxes = new JComboBox[tunableField.getGuiComboBoxAmount()]; - - for (int i = 0; i < comboBoxes.length; i++) { - TunableComboBox comboBox = new TunableComboBox(i, tunableField, eocvSim); - add(comboBox); - - comboBoxes[i] = comboBox; - } - } - - //method that should be called when this panel is added to the visualizer gui - public void showFieldPanel() { - if(hasBeenShown) return; - hasBeenShown = true; - - //updates the slider ranges from config - panelOptions.getConfigPanel().updateFieldGuiFromConfig(); - tunableField.evalRecommendedPanelMode(); - } - - public void setFieldValue(int index, Object value) { - if(index >= fields.length) return; - - String text; - if(tunableField.getAllowMode() == TunableField.AllowMode.ONLY_NUMBERS) { - text = String.valueOf((int) Math.round(Double.parseDouble(value.toString()))); - } else { - text = value.toString(); - } - - fields[index].setText(text); - - try { - sliders[index].setScaledValue(Double.parseDouble(value.toString())); - } catch(NumberFormatException ignored) {} - } - - public void setComboBoxSelection(int index, Object selection) { - comboBoxes[index].setSelectedItem(selection.toString()); - } - - protected void requestAllConfigReeval() { - reevalConfigRequested = true; - } - - public void setMode(Mode mode) { - switch(mode) { - case TEXTBOXES: - if(this.mode == Mode.SLIDERS) { - remove(slidersPanel); - } - - for(int i = 0 ; i < tunableField.getGuiFieldAmount() ; i++) { - fields[i].setInControl(true); - sliders[i].setInControl(false); - setFieldValue(i, tunableField.getGuiFieldValue(i)); - } - - add(fieldsPanel); - break; - case SLIDERS: - if(this.mode == Mode.TEXTBOXES) { - remove(fieldsPanel); - } - - for(int i = 0 ; i < tunableField.getGuiFieldAmount() ; i++) { - fields[i].setInControl(false); - sliders[i].setInControl(true); - setFieldValue(i, tunableField.getGuiFieldValue(i)); - } - - add(slidersPanel); - break; - } - - this.mode = mode; - - if(panelOptions.getMode() != mode) { - panelOptions.setMode(mode); - } - - revalidate(); repaint(); - } - - public void setSlidersRange(double min, double max) { - if(sliders == null) return; - for(TunableSlider slider : sliders) { - slider.setScaledBounds(min, max); - } - } - - public Mode getMode() { return this.mode; } - - public boolean hasRequestedAllConfigReeval() { - boolean current = reevalConfigRequested; - reevalConfigRequested = false; - - return current; - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.component.tuner; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.gui.component.tuner.element.TunableComboBox; +import com.github.serivesmejia.eocvsim.gui.component.tuner.element.TunableSlider; +import com.github.serivesmejia.eocvsim.gui.component.tuner.element.TunableTextField; +import com.github.serivesmejia.eocvsim.tuner.TunableField; + +import javax.swing.*; +import javax.swing.border.SoftBevelBorder; +import java.awt.*; + +public class TunableFieldPanel extends JPanel { + + public final TunableField tunableField; + + public TunableTextField[] fields; + public JPanel fieldsPanel; + + public TunableSlider[] sliders; + public JPanel slidersPanel; + + public JComboBox[] comboBoxes; + + public TunableFieldPanelOptions panelOptions = null; + private final EOCVSim eocvSim; + + private Mode mode; + private boolean reevalConfigRequested = false; + + private boolean hasBeenShown = false; + + public enum Mode { TEXTBOXES, SLIDERS } + + public TunableFieldPanel(TunableField tunableField, EOCVSim eocvSim) { + super(); + + this.tunableField = tunableField; + this.eocvSim = eocvSim; + + tunableField.setTunableFieldPanel(this); + + init(); + } + + private void init() { + //nice look + setBorder(new SoftBevelBorder(SoftBevelBorder.RAISED)); + + panelOptions = new TunableFieldPanelOptions(this, eocvSim); + + if(tunableField.getGuiFieldAmount() > 0) { + add(panelOptions); + } + + JLabel fieldNameLabel = new JLabel(); + fieldNameLabel.setText(tunableField.getFieldName()); + + add(fieldNameLabel); + + int fieldAmount = tunableField.getGuiFieldAmount(); + + fields = new TunableTextField[fieldAmount]; + sliders = new TunableSlider[fieldAmount]; + + fieldsPanel = new JPanel(); + slidersPanel = new JPanel(new GridBagLayout()); + + for (int i = 0 ; i < tunableField.getGuiFieldAmount() ; i++) { + //add the tunable field as a field + TunableTextField field = new TunableTextField(i, tunableField, eocvSim); + fields[i] = field; + + field.setEditable(true); + fieldsPanel.add(field); + + //add the tunable field as a slider + JLabel sliderLabel = new JLabel("0"); + TunableSlider slider = new TunableSlider(i, tunableField, eocvSim, sliderLabel); + sliders[i] = slider; + + GridBagConstraints cSlider = new GridBagConstraints(); + cSlider.gridx = 0; + cSlider.gridy = i; + + GridBagConstraints cLabel = new GridBagConstraints(); + cLabel.gridy = 1; + cLabel.gridy = i; + + slidersPanel.add(slider, cSlider); + slidersPanel.add(sliderLabel, cLabel); + } + + setMode(Mode.TEXTBOXES); + + comboBoxes = new JComboBox[tunableField.getGuiComboBoxAmount()]; + + for (int i = 0; i < comboBoxes.length; i++) { + TunableComboBox comboBox = new TunableComboBox(i, tunableField, eocvSim); + add(comboBox); + + comboBoxes[i] = comboBox; + } + } + + //method that should be called when this panel is added to the visualizer gui + public void showFieldPanel() { + if(hasBeenShown) return; + hasBeenShown = true; + + //updates the slider ranges from config + panelOptions.getConfigPanel().updateFieldGuiFromConfig(); + tunableField.evalRecommendedPanelMode(); + } + + public void setFieldValue(int index, Object value) { + if(index >= fields.length) return; + + String text; + if(tunableField.getAllowMode() == TunableField.AllowMode.ONLY_NUMBERS) { + text = String.valueOf((int) Math.round(Double.parseDouble(value.toString()))); + } else { + text = value.toString(); + } + + fields[index].setText(text); + + try { + sliders[index].setScaledValue(Double.parseDouble(value.toString())); + } catch(NumberFormatException ignored) {} + } + + public void setComboBoxSelection(int index, Object selection) { + comboBoxes[index].setSelectedItem(selection.toString()); + } + + protected void requestAllConfigReeval() { + reevalConfigRequested = true; + } + + public void setMode(Mode mode) { + switch(mode) { + case TEXTBOXES: + if(this.mode == Mode.SLIDERS) { + remove(slidersPanel); + } + + for(int i = 0 ; i < tunableField.getGuiFieldAmount() ; i++) { + fields[i].setInControl(true); + sliders[i].setInControl(false); + setFieldValue(i, tunableField.getGuiFieldValue(i)); + } + + add(fieldsPanel); + break; + case SLIDERS: + if(this.mode == Mode.TEXTBOXES) { + remove(fieldsPanel); + } + + for(int i = 0 ; i < tunableField.getGuiFieldAmount() ; i++) { + fields[i].setInControl(false); + sliders[i].setInControl(true); + setFieldValue(i, tunableField.getGuiFieldValue(i)); + } + + add(slidersPanel); + break; + } + + this.mode = mode; + + if(panelOptions.getMode() != mode) { + panelOptions.setMode(mode); + } + + revalidate(); repaint(); + } + + public void setSlidersRange(double min, double max) { + if(sliders == null) return; + for(TunableSlider slider : sliders) { + slider.setScaledBounds(min, max); + } + } + + public Mode getMode() { return this.mode; } + + public boolean hasRequestedAllConfigReeval() { + boolean current = reevalConfigRequested; + reevalConfigRequested = false; + + return current; + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelConfig.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelConfig.kt index 6facd93a..5d9b95d6 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelConfig.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelConfig.kt @@ -1,331 +1,331 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.component.tuner - -import com.github.serivesmejia.eocvsim.EOCVSim -import com.github.serivesmejia.eocvsim.gui.component.PopupX -import com.github.serivesmejia.eocvsim.gui.component.input.EnumComboBox -import com.github.serivesmejia.eocvsim.gui.component.input.SizeFields -import com.github.serivesmejia.eocvsim.tuner.TunableField -import kotlinx.coroutines.* -import kotlinx.coroutines.swing.Swing -import org.opencv.core.Size -import org.opencv.imgproc.Imgproc -import java.awt.Dimension -import java.awt.GridBagConstraints -import java.awt.GridBagLayout -import javax.swing.* -import javax.swing.border.EmptyBorder - -class TunableFieldPanelConfig(private val fieldOptions: TunableFieldPanelOptions, - private val eocvSim: EOCVSim) : JPanel() { - - var localConfig = eocvSim.config.globalTunableFieldsConfig.copy() - private set - - private var lastApplyPopup: PopupX? = null - - val currentConfig: Config - get() { - val config = localConfig.copy() - applyToConfig(config) - return config - } - - private val sliderRangeFieldsPanel = JPanel() - - private var sliderRangeFields = createRangeFields() - private val colorSpaceComboBox = EnumComboBox("Color space: ", PickerColorSpace::class.java, PickerColorSpace.values()) - - private val applyToAllButtonPanel = JPanel(GridBagLayout()) - private val applyToAllButton = JToggleButton("Apply to all fields...") - - private val applyModesPanel = JPanel() - private val applyToAllGloballyButton = JButton("Globally") - private val applyToAllOfSameTypeButton = JButton("Of this type") - - private val constCenterBottom = GridBagConstraints() - - private val configSourceLabel = JLabel(localConfig.source.description) - - private val allowsDecimals - get() = fieldOptions.fieldPanel.tunableField.allowMode == TunableField.AllowMode.ONLY_NUMBERS_DECIMAL - - private val fieldTypeClass = fieldOptions.fieldPanel.tunableField::class.java - - //represents a color space conversion when picking from the viewport. always - //convert from rgb to the desired color space since that's the color space of - //the scalar the ColorPicker returns from the viewport after picking. - enum class PickerColorSpace(val cvtCode: Int) { - YCrCb(Imgproc.COLOR_RGB2YCrCb), - HSV(Imgproc.COLOR_RGB2HSV), - RGB(Imgproc.COLOR_RGBA2RGB), - Lab(Imgproc.COLOR_RGB2Lab) - } - - enum class ConfigSource(val description: String) { - LOCAL("From local config"), - GLOBAL("From global config"), - GLOBAL_DEFAULT("From default global config"), - TYPE_SPECIFIC("From specific config") - } - - data class Config(var sliderRange: Size, - var pickerColorSpace: PickerColorSpace, - var fieldPanelMode: TunableFieldPanel.Mode, - var source: ConfigSource) - - init { - layout = GridBagLayout() - - val mConstraints = GridBagConstraints() - mConstraints.ipady = 10 - - //adding into an individual panel so that we can add - //and remove later when recreating without much problem - sliderRangeFieldsPanel.add(sliderRangeFields) - - mConstraints.gridy = 0 - add(sliderRangeFieldsPanel, mConstraints) - - colorSpaceComboBox.onSelect { updateConfigSourceLabel(currentConfig) } - //combo box to select color space - colorSpaceComboBox.selectedEnum = localConfig.pickerColorSpace - - mConstraints.gridy = 1 - add(colorSpaceComboBox, mConstraints) - - //centering apply to all button... - val constCenter = GridBagConstraints() - constCenter.anchor = GridBagConstraints.CENTER - constCenter.fill = GridBagConstraints.HORIZONTAL - constCenter.gridy = 0 - - //add apply to all button to a centered pane - applyToAllButtonPanel.add(applyToAllButton, constCenter) - - mConstraints.gridy = 2 - add(applyToAllButtonPanel, mConstraints) - - //display or hide apply to all mode buttons - applyToAllButton.addActionListener { - //create a new popup for displaying the apply modes button - if(applyToAllButton.isSelected && (lastApplyPopup == null || lastApplyPopup?.window?.isVisible == false)) { - val window = SwingUtilities.getWindowAncestor(fieldOptions) //gets the parent frame - val location = applyToAllButton.locationOnScreen - - val popup = PopupX(window, applyModesPanel, location.x, location.y) - lastApplyPopup = popup //set to a "last" variable so that we can hide it later - - //so that the main config popup doesn't get closed - //when it gets unfocused in favour of this new frame - fieldOptions.lastConfigPopup?.closeOnFocusLost = false - - popup.onShow { - popup.setLocation( - popup.window.location.x - applyModesPanel.width / 8, - popup.window.location.y + applyModesPanel.height + applyToAllButton.height - ) - } - - //untoggle the apply to all button if the popup closes - popup.onHide { - applyToAllButton.isSelected = false - - fieldOptions.lastConfigPopup?.let { - //allow the main config popup to close when losing focus now - it.closeOnFocusLost = true - - //launch the waiting in the background - GlobalScope.launch { - delay(100) - //close config popup if still hasn't focused after a bit - launch(Dispatchers.Swing) { - if (!it.window.isFocused && (lastApplyPopup == null || lastApplyPopup?.window?.isFocused == false)) { - it.hide() - } - } - } - } - } - - popup.show() - } else { - lastApplyPopup?.hide() //close the popup if user un-toggled button - } - } - - applyModesPanel.layout = BoxLayout(applyModesPanel, BoxLayout.LINE_AXIS) - - //apply globally button and disable toggle for apply to all button - applyToAllGloballyButton.addActionListener { - lastApplyPopup?.hide() - applyGlobally() - } - - applyModesPanel.add(applyToAllGloballyButton) - - //creates a space between the apply mode buttons - applyModesPanel.add(Box.createRigidArea(Dimension(5, 0))) - - //apply of same type button and disable toggle for apply to all button - applyToAllOfSameTypeButton.addActionListener { - lastApplyPopup?.hide() - applyOfSameType() - } - applyModesPanel.add(applyToAllOfSameTypeButton) - - //add a bit of space between the upper and lower apply to all buttons - applyModesPanel.border = EmptyBorder(5, 0, 0, 0) - - //add two apply to all modes buttons to the bottom center - constCenterBottom.anchor = GridBagConstraints.CENTER - constCenterBottom.fill = GridBagConstraints.HORIZONTAL - constCenterBottom.gridy = 1 - - configSourceLabel.horizontalAlignment = JLabel.CENTER - configSourceLabel.verticalAlignment = JLabel.CENTER - - mConstraints.gridy = 3 - add(configSourceLabel, mConstraints) - - applyFromEOCVSimConfig() - } - - //set the current config values and hide apply modes panel when panel show - fun panelShow() { - updateConfigGuiFromConfig() - applyToAllButton.isSelected = false - } - - //set the slider bounds when the popup gets closed - fun panelHide() { - applyToConfig() - updateFieldGuiFromConfig() - lastApplyPopup?.hide() - } - - //applies the config of this tunable field panel globally - private fun applyGlobally() { - applyToConfig() //saves the current values to the current local config - - localConfig.source = ConfigSource.GLOBAL //changes the source of the local config to global - eocvSim.config.globalTunableFieldsConfig = localConfig.copy() - - updateConfigSourceLabel() - fieldOptions.fieldPanel.requestAllConfigReeval() - } - - //applies the config of this tunable field to this type specifically - private fun applyOfSameType() { - applyToConfig() //saves the current values to the current local config - val typeClass = fieldOptions.fieldPanel.tunableField::class.java - - localConfig.source = ConfigSource.TYPE_SPECIFIC //changes the source of the local config to type specific - eocvSim.config.specificTunableFieldConfig[typeClass.name] = localConfig.copy() - - updateConfigSourceLabel() - fieldOptions.fieldPanel.requestAllConfigReeval() - } - - //loads the config from global eocv sim config file - internal fun applyFromEOCVSimConfig() { - val specificConfigs = eocvSim.config.specificTunableFieldConfig - - //apply specific config if we have one, or else, apply global - localConfig = if(specificConfigs.containsKey(fieldTypeClass.name)) { - specificConfigs[fieldTypeClass.name]!!.copy() - } else { - eocvSim.config.globalTunableFieldsConfig.copy() - } - - updateConfigGuiFromConfig() - updateConfigSourceLabel() - } - - //applies the current values to the specified config, defaults to local - @Suppress("UNNECESSARY_SAFE_CALL") - private fun applyToConfig(config: Config = localConfig) { - //if user entered a valid number and our max value is bigger than the minimum... - if(sliderRangeFields.valid) { - config.sliderRange = sliderRangeFields.currentSize - //update slider range in gui sliders... - if(config.sliderRange.height > config.sliderRange.width && config !== localConfig) - updateFieldGuiFromConfig() - } - - //set the color space enum to the config if it's not null - colorSpaceComboBox.selectedEnum?.let { - config.pickerColorSpace = it - } - - //sets the panel mode (sliders or textboxes) to config from the current mode - if(fieldOptions.fieldPanel?.mode != null) { - config.fieldPanelMode = fieldOptions.fieldPanel.mode - } - } - - private fun updateConfigSourceLabel(currentConfig: Config = localConfig) { - //sets to local if user changed values and hasn't applied locally or globally - if(currentConfig != localConfig) { - localConfig.source = ConfigSource.LOCAL - } - - configSourceLabel.text = localConfig.source.description - } - - //updates the actual configuration displayed on the field panel gui - @Suppress("UNNECESSARY_SAFE_CALL") - fun updateFieldGuiFromConfig() { - //sets the slider range from config - fieldOptions.fieldPanel.setSlidersRange(localConfig.sliderRange.width, localConfig.sliderRange.height) - //sets the panel mode (sliders or textboxes) to config from the current mode - if(fieldOptions.fieldPanel?.fields != null){ - fieldOptions.fieldPanel.mode = localConfig.fieldPanelMode - } - } - - //updates the values displayed in this config's ui to the current config values - private fun updateConfigGuiFromConfig() { - sliderRangeFieldsPanel.remove(sliderRangeFields) //remove old fields - sliderRangeFields = createRangeFields() //need to recreate in order to set new values - sliderRangeFieldsPanel.add(sliderRangeFields) //add new fields - - //need to reval&repaint as always - sliderRangeFieldsPanel.revalidate(); sliderRangeFieldsPanel.repaint() - - colorSpaceComboBox.selectedEnum = localConfig.pickerColorSpace - } - - //simple short hand for a repetitive instantiation... - private fun createRangeFields(): SizeFields { - val fields = SizeFields(localConfig.sliderRange, allowsDecimals, true,"Slider range:", " to ") - fields.onChange { - updateConfigSourceLabel(currentConfig) - } - - return fields - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.component.tuner + +import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.gui.component.PopupX +import com.github.serivesmejia.eocvsim.gui.component.input.EnumComboBox +import com.github.serivesmejia.eocvsim.gui.component.input.SizeFields +import com.github.serivesmejia.eocvsim.tuner.TunableField +import kotlinx.coroutines.* +import kotlinx.coroutines.swing.Swing +import org.opencv.core.Size +import org.opencv.imgproc.Imgproc +import java.awt.Dimension +import java.awt.GridBagConstraints +import java.awt.GridBagLayout +import javax.swing.* +import javax.swing.border.EmptyBorder + +class TunableFieldPanelConfig(private val fieldOptions: TunableFieldPanelOptions, + private val eocvSim: EOCVSim) : JPanel() { + + var localConfig = eocvSim.config.globalTunableFieldsConfig.copy() + private set + + private var lastApplyPopup: PopupX? = null + + val currentConfig: Config + get() { + val config = localConfig.copy() + applyToConfig(config) + return config + } + + private val sliderRangeFieldsPanel = JPanel() + + private var sliderRangeFields = createRangeFields() + private val colorSpaceComboBox = EnumComboBox("Color space: ", PickerColorSpace::class.java, PickerColorSpace.values()) + + private val applyToAllButtonPanel = JPanel(GridBagLayout()) + private val applyToAllButton = JToggleButton("Apply to all fields...") + + private val applyModesPanel = JPanel() + private val applyToAllGloballyButton = JButton("Globally") + private val applyToAllOfSameTypeButton = JButton("Of this type") + + private val constCenterBottom = GridBagConstraints() + + private val configSourceLabel = JLabel(localConfig.source.description) + + private val allowsDecimals + get() = fieldOptions.fieldPanel.tunableField.allowMode == TunableField.AllowMode.ONLY_NUMBERS_DECIMAL + + private val fieldTypeClass = fieldOptions.fieldPanel.tunableField::class.java + + //represents a color space conversion when picking from the viewport. always + //convert from rgb to the desired color space since that's the color space of + //the scalar the ColorPicker returns from the viewport after picking. + enum class PickerColorSpace(val cvtCode: Int) { + YCrCb(Imgproc.COLOR_RGB2YCrCb), + HSV(Imgproc.COLOR_RGB2HSV), + RGB(Imgproc.COLOR_RGBA2RGB), + Lab(Imgproc.COLOR_RGB2Lab) + } + + enum class ConfigSource(val description: String) { + LOCAL("From local config"), + GLOBAL("From global config"), + GLOBAL_DEFAULT("From default global config"), + TYPE_SPECIFIC("From specific config") + } + + data class Config(var sliderRange: Size, + var pickerColorSpace: PickerColorSpace, + var fieldPanelMode: TunableFieldPanel.Mode, + var source: ConfigSource) + + init { + layout = GridBagLayout() + + val mConstraints = GridBagConstraints() + mConstraints.ipady = 10 + + //adding into an individual panel so that we can add + //and remove later when recreating without much problem + sliderRangeFieldsPanel.add(sliderRangeFields) + + mConstraints.gridy = 0 + add(sliderRangeFieldsPanel, mConstraints) + + colorSpaceComboBox.onSelect { updateConfigSourceLabel(currentConfig) } + //combo box to select color space + colorSpaceComboBox.selectedEnum = localConfig.pickerColorSpace + + mConstraints.gridy = 1 + add(colorSpaceComboBox, mConstraints) + + //centering apply to all button... + val constCenter = GridBagConstraints() + constCenter.anchor = GridBagConstraints.CENTER + constCenter.fill = GridBagConstraints.HORIZONTAL + constCenter.gridy = 0 + + //add apply to all button to a centered pane + applyToAllButtonPanel.add(applyToAllButton, constCenter) + + mConstraints.gridy = 2 + add(applyToAllButtonPanel, mConstraints) + + //display or hide apply to all mode buttons + applyToAllButton.addActionListener { + //create a new popup for displaying the apply modes button + if(applyToAllButton.isSelected && (lastApplyPopup == null || lastApplyPopup?.window?.isVisible == false)) { + val window = SwingUtilities.getWindowAncestor(fieldOptions) //gets the parent frame + val location = applyToAllButton.locationOnScreen + + val popup = PopupX(window, applyModesPanel, location.x, location.y) + lastApplyPopup = popup //set to a "last" variable so that we can hide it later + + //so that the main config popup doesn't get closed + //when it gets unfocused in favour of this new frame + fieldOptions.lastConfigPopup?.closeOnFocusLost = false + + popup.onShow { + popup.setLocation( + popup.window.location.x - applyModesPanel.width / 8, + popup.window.location.y + applyModesPanel.height + applyToAllButton.height + ) + } + + //untoggle the apply to all button if the popup closes + popup.onHide { + applyToAllButton.isSelected = false + + fieldOptions.lastConfigPopup?.let { + //allow the main config popup to close when losing focus now + it.closeOnFocusLost = true + + //launch the waiting in the background + GlobalScope.launch { + delay(100) + //close config popup if still hasn't focused after a bit + launch(Dispatchers.Swing) { + if (!it.window.isFocused && (lastApplyPopup == null || lastApplyPopup?.window?.isFocused == false)) { + it.hide() + } + } + } + } + } + + popup.show() + } else { + lastApplyPopup?.hide() //close the popup if user un-toggled button + } + } + + applyModesPanel.layout = BoxLayout(applyModesPanel, BoxLayout.LINE_AXIS) + + //apply globally button and disable toggle for apply to all button + applyToAllGloballyButton.addActionListener { + lastApplyPopup?.hide() + applyGlobally() + } + + applyModesPanel.add(applyToAllGloballyButton) + + //creates a space between the apply mode buttons + applyModesPanel.add(Box.createRigidArea(Dimension(5, 0))) + + //apply of same type button and disable toggle for apply to all button + applyToAllOfSameTypeButton.addActionListener { + lastApplyPopup?.hide() + applyOfSameType() + } + applyModesPanel.add(applyToAllOfSameTypeButton) + + //add a bit of space between the upper and lower apply to all buttons + applyModesPanel.border = EmptyBorder(5, 0, 0, 0) + + //add two apply to all modes buttons to the bottom center + constCenterBottom.anchor = GridBagConstraints.CENTER + constCenterBottom.fill = GridBagConstraints.HORIZONTAL + constCenterBottom.gridy = 1 + + configSourceLabel.horizontalAlignment = JLabel.CENTER + configSourceLabel.verticalAlignment = JLabel.CENTER + + mConstraints.gridy = 3 + add(configSourceLabel, mConstraints) + + applyFromEOCVSimConfig() + } + + //set the current config values and hide apply modes panel when panel show + fun panelShow() { + updateConfigGuiFromConfig() + applyToAllButton.isSelected = false + } + + //set the slider bounds when the popup gets closed + fun panelHide() { + applyToConfig() + updateFieldGuiFromConfig() + lastApplyPopup?.hide() + } + + //applies the config of this tunable field panel globally + private fun applyGlobally() { + applyToConfig() //saves the current values to the current local config + + localConfig.source = ConfigSource.GLOBAL //changes the source of the local config to global + eocvSim.config.globalTunableFieldsConfig = localConfig.copy() + + updateConfigSourceLabel() + fieldOptions.fieldPanel.requestAllConfigReeval() + } + + //applies the config of this tunable field to this type specifically + private fun applyOfSameType() { + applyToConfig() //saves the current values to the current local config + val typeClass = fieldOptions.fieldPanel.tunableField::class.java + + localConfig.source = ConfigSource.TYPE_SPECIFIC //changes the source of the local config to type specific + eocvSim.config.specificTunableFieldConfig[typeClass.name] = localConfig.copy() + + updateConfigSourceLabel() + fieldOptions.fieldPanel.requestAllConfigReeval() + } + + //loads the config from global eocv sim config file + internal fun applyFromEOCVSimConfig() { + val specificConfigs = eocvSim.config.specificTunableFieldConfig + + //apply specific config if we have one, or else, apply global + localConfig = if(specificConfigs.containsKey(fieldTypeClass.name)) { + specificConfigs[fieldTypeClass.name]!!.copy() + } else { + eocvSim.config.globalTunableFieldsConfig.copy() + } + + updateConfigGuiFromConfig() + updateConfigSourceLabel() + } + + //applies the current values to the specified config, defaults to local + @Suppress("UNNECESSARY_SAFE_CALL") + private fun applyToConfig(config: Config = localConfig) { + //if user entered a valid number and our max value is bigger than the minimum... + if(sliderRangeFields.valid) { + config.sliderRange = sliderRangeFields.currentSize + //update slider range in gui sliders... + if(config.sliderRange.height > config.sliderRange.width && config !== localConfig) + updateFieldGuiFromConfig() + } + + //set the color space enum to the config if it's not null + colorSpaceComboBox.selectedEnum?.let { + config.pickerColorSpace = it + } + + //sets the panel mode (sliders or textboxes) to config from the current mode + if(fieldOptions.fieldPanel?.mode != null) { + config.fieldPanelMode = fieldOptions.fieldPanel.mode + } + } + + private fun updateConfigSourceLabel(currentConfig: Config = localConfig) { + //sets to local if user changed values and hasn't applied locally or globally + if(currentConfig != localConfig) { + localConfig.source = ConfigSource.LOCAL + } + + configSourceLabel.text = localConfig.source.description + } + + //updates the actual configuration displayed on the field panel gui + @Suppress("UNNECESSARY_SAFE_CALL") + fun updateFieldGuiFromConfig() { + //sets the slider range from config + fieldOptions.fieldPanel.setSlidersRange(localConfig.sliderRange.width, localConfig.sliderRange.height) + //sets the panel mode (sliders or textboxes) to config from the current mode + if(fieldOptions.fieldPanel?.fields != null){ + fieldOptions.fieldPanel.mode = localConfig.fieldPanelMode + } + } + + //updates the values displayed in this config's ui to the current config values + private fun updateConfigGuiFromConfig() { + sliderRangeFieldsPanel.remove(sliderRangeFields) //remove old fields + sliderRangeFields = createRangeFields() //need to recreate in order to set new values + sliderRangeFieldsPanel.add(sliderRangeFields) //add new fields + + //need to reval&repaint as always + sliderRangeFieldsPanel.revalidate(); sliderRangeFieldsPanel.repaint() + + colorSpaceComboBox.selectedEnum = localConfig.pickerColorSpace + } + + //simple short hand for a repetitive instantiation... + private fun createRangeFields(): SizeFields { + val fields = SizeFields(localConfig.sliderRange, allowsDecimals, true,"Slider range:", " to ") + fields.onChange { + updateConfigSourceLabel(currentConfig) + } + + return fields + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelOptions.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelOptions.kt index 6dc1e481..9be6461a 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelOptions.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelOptions.kt @@ -1,195 +1,195 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.component.tuner - -import com.github.serivesmejia.eocvsim.EOCVSim -import com.github.serivesmejia.eocvsim.gui.Icons -import com.github.serivesmejia.eocvsim.gui.component.PopupX -import com.github.serivesmejia.eocvsim.util.extension.cvtColor -import com.github.serivesmejia.eocvsim.util.extension.clipUpperZero -import java.awt.FlowLayout -import java.awt.GridLayout -import java.awt.event.ComponentAdapter -import java.awt.event.ComponentEvent -import javax.swing.* -import javax.swing.event.AncestorEvent -import javax.swing.event.AncestorListener - -class TunableFieldPanelOptions(val fieldPanel: TunableFieldPanel, - eocvSim: EOCVSim) : JPanel() { - - private val sliderIco by Icons.lazyGetImageResized("ico_slider", 15, 15) - private val textBoxIco by Icons.lazyGetImageResized("ico_textbox", 15, 15) - private val configIco by Icons.lazyGetImageResized("ico_config", 15, 15) - private val colorPickIco by Icons.lazyGetImageResized("ico_colorpick", 15, 15) - - private val textBoxSliderToggle = JToggleButton() - private val configButton = JButton() - private val colorPickButton = JToggleButton() - - val configPanel = TunableFieldPanelConfig(this, eocvSim) - var lastConfigPopup: PopupX? = null - private set - - //toggle between textbox and slider ico - var mode = TunableFieldPanel.Mode.TEXTBOXES - set(value) { - when(value) { - TunableFieldPanel.Mode.SLIDERS -> { - textBoxSliderToggle.icon = textBoxIco - textBoxSliderToggle.isSelected = true - } - TunableFieldPanel.Mode.TEXTBOXES -> { - textBoxSliderToggle.icon = sliderIco - textBoxSliderToggle.isSelected = false - } - } - - handleResize() - - if(fieldPanel.mode != value) fieldPanel.mode = value - configPanel.localConfig.fieldPanelMode = value - - field = value - } - - init { - //set initial icon for buttons - textBoxSliderToggle.icon = sliderIco - configButton.icon = configIco - colorPickButton.icon = colorPickIco - - add(textBoxSliderToggle) - add(configButton) - add(colorPickButton) - - textBoxSliderToggle.addActionListener { - mode = if(textBoxSliderToggle.isSelected) { - TunableFieldPanel.Mode.SLIDERS - } else { - TunableFieldPanel.Mode.TEXTBOXES - } - configPanel.localConfig.fieldPanelMode = mode - } - - configButton.addActionListener { - val configLocation = configButton.locationOnScreen - val buttonHeight = configButton.height / 2 - - val window = SwingUtilities.getWindowAncestor(this) - val popup = PopupX(window, configPanel, configLocation.x, configLocation.y - buttonHeight) - - popup.onShow.doOnce { configPanel.panelShow() } - popup.onHide.doOnce { configPanel.panelHide() } - - //make sure we hide last config so - //that we don't get a "stuck" popup - //if the silly user is pressing the - //button wayy too fast - lastConfigPopup?.hide() - lastConfigPopup = popup - - popup.show() - } - - colorPickButton.addActionListener { - val colorPicker = fieldPanel.tunableField.eocvSim.visualizer.colorPicker - - //start picking if global color picker is not being used by other panel - if(!colorPicker.isPicking && colorPickButton.isSelected) { - startPicking(colorPicker) - } else { //handles cases when cancelling picking - colorPicker.stopPicking() - //if we weren't the ones controlling the last picking, - //start picking again to gain control for this panel - if(colorPickButton.isSelected) startPicking(colorPicker) - } - } - - fieldPanel.addComponentListener(object: ComponentAdapter() { - override fun componentResized(e: ComponentEvent?) = handleResize() - }) - - addAncestorListener(object: AncestorListener { - override fun ancestorRemoved(event: AncestorEvent?) {} - override fun ancestorMoved(event: AncestorEvent?) {} - - override fun ancestorAdded(event: AncestorEvent?) = handleResize() - }) - } - - private fun startPicking(colorPicker: ColorPicker) { - //when user picks a color - colorPicker.onPick.doOnce { - val colorScalar = colorPicker.colorRgb.cvtColor(configPanel.localConfig.pickerColorSpace.cvtCode) - - //setting the scalar value in order from first to fourth field - for(i in 0..(fieldPanel.fields.size - 1).clipUpperZero()) { - //if we're still in range of the scalar values amount - if(i < colorScalar.`val`.size) { - val colorVal = colorScalar.`val`[i] - fieldPanel.setFieldValue(i, colorVal) - fieldPanel.tunableField.setGuiFieldValue(i, colorVal.toString()) - } else { break } //keep looping until we write the entire scalar value - } - colorPickButton.isSelected = false - } - - //handles cancel cases, mostly when passing control to another panel - colorPicker.onCancel.doOnce { colorPickButton.isSelected = false } - - //might want to start picking to this panel here... - colorPicker.startPicking() - } - - //handling resizes for responsive buttons arrangement - private fun handleResize() { - val buttonsHeight = textBoxSliderToggle.height + colorPickButton.height + configButton.height - - layout = if(fieldPanel.height > buttonsHeight && mode == TunableFieldPanel.Mode.SLIDERS) { - GridLayout(3, 1) - } else { - FlowLayout() - } - - revalAndRepaint() - } - - //reevaluates the config of this field panel from the eocv sim config - fun reevaluateConfig() { - //only reevaluate if our config is not local - if(configPanel.localConfig.source != TunableFieldPanelConfig.ConfigSource.LOCAL) { - configPanel.applyFromEOCVSimConfig() - } - } - - private fun revalAndRepaint() { - textBoxSliderToggle.revalidate() - textBoxSliderToggle.repaint() - - revalidate() - repaint() - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.component.tuner + +import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.gui.Icons +import com.github.serivesmejia.eocvsim.gui.component.PopupX +import com.github.serivesmejia.eocvsim.util.extension.cvtColor +import com.github.serivesmejia.eocvsim.util.extension.clipUpperZero +import java.awt.FlowLayout +import java.awt.GridLayout +import java.awt.event.ComponentAdapter +import java.awt.event.ComponentEvent +import javax.swing.* +import javax.swing.event.AncestorEvent +import javax.swing.event.AncestorListener + +class TunableFieldPanelOptions(val fieldPanel: TunableFieldPanel, + eocvSim: EOCVSim) : JPanel() { + + private val sliderIco by Icons.lazyGetImageResized("ico_slider", 15, 15) + private val textBoxIco by Icons.lazyGetImageResized("ico_textbox", 15, 15) + private val configIco by Icons.lazyGetImageResized("ico_config", 15, 15) + private val colorPickIco by Icons.lazyGetImageResized("ico_colorpick", 15, 15) + + private val textBoxSliderToggle = JToggleButton() + private val configButton = JButton() + private val colorPickButton = JToggleButton() + + val configPanel = TunableFieldPanelConfig(this, eocvSim) + var lastConfigPopup: PopupX? = null + private set + + //toggle between textbox and slider ico + var mode = TunableFieldPanel.Mode.TEXTBOXES + set(value) { + when(value) { + TunableFieldPanel.Mode.SLIDERS -> { + textBoxSliderToggle.icon = textBoxIco + textBoxSliderToggle.isSelected = true + } + TunableFieldPanel.Mode.TEXTBOXES -> { + textBoxSliderToggle.icon = sliderIco + textBoxSliderToggle.isSelected = false + } + } + + handleResize() + + if(fieldPanel.mode != value) fieldPanel.mode = value + configPanel.localConfig.fieldPanelMode = value + + field = value + } + + init { + //set initial icon for buttons + textBoxSliderToggle.icon = sliderIco + configButton.icon = configIco + colorPickButton.icon = colorPickIco + + add(textBoxSliderToggle) + add(configButton) + add(colorPickButton) + + textBoxSliderToggle.addActionListener { + mode = if(textBoxSliderToggle.isSelected) { + TunableFieldPanel.Mode.SLIDERS + } else { + TunableFieldPanel.Mode.TEXTBOXES + } + configPanel.localConfig.fieldPanelMode = mode + } + + configButton.addActionListener { + val configLocation = configButton.locationOnScreen + val buttonHeight = configButton.height / 2 + + val window = SwingUtilities.getWindowAncestor(this) + val popup = PopupX(window, configPanel, configLocation.x, configLocation.y - buttonHeight) + + popup.onShow.doOnce { configPanel.panelShow() } + popup.onHide.doOnce { configPanel.panelHide() } + + //make sure we hide last config so + //that we don't get a "stuck" popup + //if the silly user is pressing the + //button wayy too fast + lastConfigPopup?.hide() + lastConfigPopup = popup + + popup.show() + } + + colorPickButton.addActionListener { + val colorPicker = fieldPanel.tunableField.eocvSim.visualizer.colorPicker + + //start picking if global color picker is not being used by other panel + if(!colorPicker.isPicking && colorPickButton.isSelected) { + startPicking(colorPicker) + } else { //handles cases when cancelling picking + colorPicker.stopPicking() + //if we weren't the ones controlling the last picking, + //start picking again to gain control for this panel + if(colorPickButton.isSelected) startPicking(colorPicker) + } + } + + fieldPanel.addComponentListener(object: ComponentAdapter() { + override fun componentResized(e: ComponentEvent?) = handleResize() + }) + + addAncestorListener(object: AncestorListener { + override fun ancestorRemoved(event: AncestorEvent?) {} + override fun ancestorMoved(event: AncestorEvent?) {} + + override fun ancestorAdded(event: AncestorEvent?) = handleResize() + }) + } + + private fun startPicking(colorPicker: ColorPicker) { + //when user picks a color + colorPicker.onPick.doOnce { + val colorScalar = colorPicker.colorRgb.cvtColor(configPanel.localConfig.pickerColorSpace.cvtCode) + + //setting the scalar value in order from first to fourth field + for(i in 0..(fieldPanel.fields.size - 1).clipUpperZero()) { + //if we're still in range of the scalar values amount + if(i < colorScalar.`val`.size) { + val colorVal = colorScalar.`val`[i] + fieldPanel.setFieldValue(i, colorVal) + fieldPanel.tunableField.setGuiFieldValue(i, colorVal.toString()) + } else { break } //keep looping until we write the entire scalar value + } + colorPickButton.isSelected = false + } + + //handles cancel cases, mostly when passing control to another panel + colorPicker.onCancel.doOnce { colorPickButton.isSelected = false } + + //might want to start picking to this panel here... + colorPicker.startPicking() + } + + //handling resizes for responsive buttons arrangement + private fun handleResize() { + val buttonsHeight = textBoxSliderToggle.height + colorPickButton.height + configButton.height + + layout = if(fieldPanel.height > buttonsHeight && mode == TunableFieldPanel.Mode.SLIDERS) { + GridLayout(3, 1) + } else { + FlowLayout() + } + + revalAndRepaint() + } + + //reevaluates the config of this field panel from the eocv sim config + fun reevaluateConfig() { + //only reevaluate if our config is not local + if(configPanel.localConfig.source != TunableFieldPanelConfig.ConfigSource.LOCAL) { + configPanel.applyFromEOCVSimConfig() + } + } + + private fun revalAndRepaint() { + textBoxSliderToggle.revalidate() + textBoxSliderToggle.repaint() + + revalidate() + repaint() + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/element/TunableComboBox.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/element/TunableComboBox.java index acfae9cd..7e0ef8ed 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/element/TunableComboBox.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/element/TunableComboBox.java @@ -1,67 +1,67 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.component.tuner.element; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.tuner.TunableField; - -import javax.swing.*; -import java.util.Objects; - -public class TunableComboBox extends JComboBox { - - private final TunableField tunableField; - private final int index; - - private final EOCVSim eocvSim; - - public TunableComboBox(int index, TunableField tunableField, EOCVSim eocvSim) { - super(); - - this.tunableField = tunableField; - this.index = index; - this.eocvSim = eocvSim; - - init(); - } - - private void init() { - for (Object obj : tunableField.getGuiComboBoxValues(index)) { - this.addItem(obj.toString()); - } - - addItemListener(evt -> eocvSim.onMainUpdate.doOnce(() -> { - try { - tunableField.setGuiComboBoxValue(index, Objects.requireNonNull(getSelectedItem()).toString()); - } catch (IllegalAccessException ex) { - ex.printStackTrace(); - } - - if (eocvSim.pipelineManager.getPaused()) { - eocvSim.pipelineManager.requestSetPaused(false); - } - })); - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.component.tuner.element; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.tuner.TunableField; + +import javax.swing.*; +import java.util.Objects; + +public class TunableComboBox extends JComboBox { + + private final TunableField tunableField; + private final int index; + + private final EOCVSim eocvSim; + + public TunableComboBox(int index, TunableField tunableField, EOCVSim eocvSim) { + super(); + + this.tunableField = tunableField; + this.index = index; + this.eocvSim = eocvSim; + + init(); + } + + private void init() { + for (Object obj : tunableField.getGuiComboBoxValues(index)) { + this.addItem(obj.toString()); + } + + addItemListener(evt -> eocvSim.onMainUpdate.doOnce(() -> { + try { + tunableField.setGuiComboBoxValue(index, Objects.requireNonNull(getSelectedItem()).toString()); + } catch (IllegalAccessException ex) { + ex.printStackTrace(); + } + + if (eocvSim.pipelineManager.getPaused()) { + eocvSim.pipelineManager.requestSetPaused(false); + } + })); + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/element/TunableSlider.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/element/TunableSlider.kt index c630e6ba..170a1f6d 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/element/TunableSlider.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/element/TunableSlider.kt @@ -1,76 +1,76 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.component.tuner.element - -import com.github.serivesmejia.eocvsim.EOCVSim -import com.github.serivesmejia.eocvsim.gui.component.SliderX -import com.github.serivesmejia.eocvsim.tuner.TunableField -import com.github.serivesmejia.eocvsim.util.event.EventListener -import javax.swing.JLabel -import kotlin.math.roundToInt - -class TunableSlider(val index: Int, - val tunableField: TunableField<*>, - val eocvSim: EOCVSim, - val valueLabel: JLabel? = null, - minBound: Double = 0.0, - maxBound: Double = 255.0) : SliderX(minBound, maxBound, 10) { - - var inControl = false - - constructor(i: Int, tunableField: TunableField, eocvSim: EOCVSim, valueLabel: JLabel) : this(i, tunableField, eocvSim, valueLabel, 0.0, 255.0) - - constructor(i: Int, tunableField: TunableField, eocvSim: EOCVSim) : this(i, tunableField, eocvSim, null, 0.0, 255.0) - - private val changeFieldValue = EventListener { - if(inControl) { - tunableField.setGuiFieldValue(index, scaledValue.toString()) - - if (eocvSim.pipelineManager.paused) - eocvSim.pipelineManager.setPaused(false) - } - } - - init { - - addChangeListener { - eocvSim.onMainUpdate.doOnce(changeFieldValue) - - valueLabel?.text = if (tunableField.allowMode == TunableField.AllowMode.ONLY_NUMBERS_DECIMAL) { - scaledValue.toString() - } else { - scaledValue.roundToInt().toString() - } - } - - tunableField.onValueChange { - if (!inControl) { - scaledValue = try { - tunableField.getGuiFieldValue(index).toString().toDouble() - } catch(ignored: NumberFormatException) { 0.0 } - } - } - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.component.tuner.element + +import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.gui.component.SliderX +import com.github.serivesmejia.eocvsim.tuner.TunableField +import com.github.serivesmejia.eocvsim.util.event.EventListener +import javax.swing.JLabel +import kotlin.math.roundToInt + +class TunableSlider(val index: Int, + val tunableField: TunableField<*>, + val eocvSim: EOCVSim, + val valueLabel: JLabel? = null, + minBound: Double = 0.0, + maxBound: Double = 255.0) : SliderX(minBound, maxBound, 10) { + + var inControl = false + + constructor(i: Int, tunableField: TunableField, eocvSim: EOCVSim, valueLabel: JLabel) : this(i, tunableField, eocvSim, valueLabel, 0.0, 255.0) + + constructor(i: Int, tunableField: TunableField, eocvSim: EOCVSim) : this(i, tunableField, eocvSim, null, 0.0, 255.0) + + private val changeFieldValue = EventListener { + if(inControl) { + tunableField.setGuiFieldValue(index, scaledValue.toString()) + + if (eocvSim.pipelineManager.paused) + eocvSim.pipelineManager.setPaused(false) + } + } + + init { + + addChangeListener { + eocvSim.onMainUpdate.doOnce(changeFieldValue) + + valueLabel?.text = if (tunableField.allowMode == TunableField.AllowMode.ONLY_NUMBERS_DECIMAL) { + scaledValue.toString() + } else { + scaledValue.roundToInt().toString() + } + } + + tunableField.onValueChange { + if (!inControl) { + scaledValue = try { + tunableField.getGuiFieldValue(index).toString().toDouble() + } catch(ignored: NumberFormatException) { 0.0 } + } + } + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/element/TunableTextField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/element/TunableTextField.java index b5a479fb..5088cee1 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/element/TunableTextField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/element/TunableTextField.java @@ -1,191 +1,191 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.component.tuner.element; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.tuner.TunableField; - -import javax.swing.*; -import javax.swing.border.Border; -import javax.swing.border.LineBorder; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import javax.swing.text.AbstractDocument; -import javax.swing.text.AttributeSet; -import javax.swing.text.BadLocationException; -import javax.swing.text.DocumentFilter; -import java.awt.*; -import java.awt.event.KeyEvent; -import java.awt.event.KeyListener; -import java.util.ArrayList; -import java.util.Collections; - -public class TunableTextField extends JTextField { - - private final ArrayList validCharsIfNumber = new ArrayList<>(); - - private final TunableField tunableField; - private final int index; - private final EOCVSim eocvSim; - - private final Border initialBorder; - - private volatile boolean hasValidText = true; - - private boolean inControl = false; - - public TunableTextField(int index, TunableField tunableField, EOCVSim eocvSim) { - super(); - - this.initialBorder = this.getBorder(); - - this.tunableField = tunableField; - this.index = index; - this.eocvSim = eocvSim; - - setText(tunableField.getGuiFieldValue(index).toString()); - - int plusW = Math.round(getText().length() / 5f) * 10; - this.setPreferredSize(new Dimension(40 + plusW, getPreferredSize().height)); - - tunableField.onValueChange.doPersistent(() -> { - if(!inControl) { - setText(tunableField.getGuiFieldValue(index).toString()); - } - }); - - if (tunableField.isOnlyNumbers()) { - - //add all valid characters for non decimal numeric fields - Collections.addAll(validCharsIfNumber, '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-'); - - //allow dots for decimal numeric fields - if (tunableField.getAllowMode() == TunableField.AllowMode.ONLY_NUMBERS_DECIMAL) { - validCharsIfNumber.add('.'); - } - - ((AbstractDocument) getDocument()).setDocumentFilter(new DocumentFilter() { - - @Override - public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException { - text = text.replace(" ", ""); - - for (char c : text.toCharArray()) { - if (!isNumberCharacter(c)) return; - } - - boolean invalidNumber = false; - - try { //check if entered text is valid number - Double.valueOf(text); - } catch (NumberFormatException ex) { - invalidNumber = true; - } - - hasValidText = !invalidNumber || !text.isEmpty(); - - if (hasValidText) { - setNormalBorder(); - } else { - setRedBorder(); - } - - super.replace(fb, offset, length, text, attrs); - } - - }); - - } - - getDocument().addDocumentListener(new DocumentListener() { - - Runnable changeFieldValue = () -> { - if ((!hasValidText || !tunableField.isOnlyNumbers() || !getText().trim().equals(""))) { - try { - tunableField.setGuiFieldValue(index, getText()); - } catch (Exception e) { - setRedBorder(); - } - } else { - setRedBorder(); - } - }; - - @Override - public void insertUpdate(DocumentEvent e) { - change(); - } - @Override - public void removeUpdate(DocumentEvent e) { change(); } - @Override - public void changedUpdate(DocumentEvent e) { change(); } - - public void change() { - eocvSim.onMainUpdate.doOnce(changeFieldValue); - } - }); - - //unpausing when typing on any tunable text box - addKeyListener(new KeyListener() { - @Override - public void keyTyped(KeyEvent e) { - execute(); - } - @Override - public void keyPressed(KeyEvent e) { - execute(); - } - @Override - public void keyReleased(KeyEvent e) { execute(); } - - public void execute() { - if (eocvSim.pipelineManager.getPaused()) { - eocvSim.pipelineManager.requestSetPaused(false); - } - } - }); - } - - public void setNormalBorder() { - setBorder(initialBorder); - } - - public void setRedBorder() { - setBorder(new LineBorder(new Color(255, 79, 79), 2)); - } - - public void setInControl(boolean inControl) { - this.inControl = inControl; - } - - private boolean isNumberCharacter(char c) { - for (char validC : validCharsIfNumber) { - if (c == validC) return true; - } - return false; - } - - public boolean isInControl() { return inControl; } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.component.tuner.element; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.tuner.TunableField; + +import javax.swing.*; +import javax.swing.border.Border; +import javax.swing.border.LineBorder; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.AbstractDocument; +import javax.swing.text.AttributeSet; +import javax.swing.text.BadLocationException; +import javax.swing.text.DocumentFilter; +import java.awt.*; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.util.ArrayList; +import java.util.Collections; + +public class TunableTextField extends JTextField { + + private final ArrayList validCharsIfNumber = new ArrayList<>(); + + private final TunableField tunableField; + private final int index; + private final EOCVSim eocvSim; + + private final Border initialBorder; + + private volatile boolean hasValidText = true; + + private boolean inControl = false; + + public TunableTextField(int index, TunableField tunableField, EOCVSim eocvSim) { + super(); + + this.initialBorder = this.getBorder(); + + this.tunableField = tunableField; + this.index = index; + this.eocvSim = eocvSim; + + setText(tunableField.getGuiFieldValue(index).toString()); + + int plusW = Math.round(getText().length() / 5f) * 10; + this.setPreferredSize(new Dimension(40 + plusW, getPreferredSize().height)); + + tunableField.onValueChange.doPersistent(() -> { + if(!inControl) { + setText(tunableField.getGuiFieldValue(index).toString()); + } + }); + + if (tunableField.isOnlyNumbers()) { + + //add all valid characters for non decimal numeric fields + Collections.addAll(validCharsIfNumber, '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-'); + + //allow dots for decimal numeric fields + if (tunableField.getAllowMode() == TunableField.AllowMode.ONLY_NUMBERS_DECIMAL) { + validCharsIfNumber.add('.'); + } + + ((AbstractDocument) getDocument()).setDocumentFilter(new DocumentFilter() { + + @Override + public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException { + text = text.replace(" ", ""); + + for (char c : text.toCharArray()) { + if (!isNumberCharacter(c)) return; + } + + boolean invalidNumber = false; + + try { //check if entered text is valid number + Double.valueOf(text); + } catch (NumberFormatException ex) { + invalidNumber = true; + } + + hasValidText = !invalidNumber || !text.isEmpty(); + + if (hasValidText) { + setNormalBorder(); + } else { + setRedBorder(); + } + + super.replace(fb, offset, length, text, attrs); + } + + }); + + } + + getDocument().addDocumentListener(new DocumentListener() { + + Runnable changeFieldValue = () -> { + if ((!hasValidText || !tunableField.isOnlyNumbers() || !getText().trim().equals(""))) { + try { + tunableField.setGuiFieldValue(index, getText()); + } catch (Exception e) { + setRedBorder(); + } + } else { + setRedBorder(); + } + }; + + @Override + public void insertUpdate(DocumentEvent e) { + change(); + } + @Override + public void removeUpdate(DocumentEvent e) { change(); } + @Override + public void changedUpdate(DocumentEvent e) { change(); } + + public void change() { + eocvSim.onMainUpdate.doOnce(changeFieldValue); + } + }); + + //unpausing when typing on any tunable text box + addKeyListener(new KeyListener() { + @Override + public void keyTyped(KeyEvent e) { + execute(); + } + @Override + public void keyPressed(KeyEvent e) { + execute(); + } + @Override + public void keyReleased(KeyEvent e) { execute(); } + + public void execute() { + if (eocvSim.pipelineManager.getPaused()) { + eocvSim.pipelineManager.requestSetPaused(false); + } + } + }); + } + + public void setNormalBorder() { + setBorder(initialBorder); + } + + public void setRedBorder() { + setBorder(new LineBorder(new Color(255, 79, 79), 2)); + } + + public void setInControl(boolean inControl) { + this.inControl = inControl; + } + + private boolean isNumberCharacter(char c) { + for (char validC : validCharsIfNumber) { + if (c == validC) return true; + } + return false; + } + + public boolean isInControl() { return inControl; } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/CreateSourcePanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/CreateSourcePanel.kt index 80f1324d..e80521fe 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/CreateSourcePanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/CreateSourcePanel.kt @@ -1,65 +1,65 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.component.visualizer - -import com.github.serivesmejia.eocvsim.EOCVSim -import com.github.serivesmejia.eocvsim.gui.DialogFactory -import com.github.serivesmejia.eocvsim.gui.component.PopupX -import com.github.serivesmejia.eocvsim.gui.component.input.EnumComboBox -import com.github.serivesmejia.eocvsim.input.SourceType -import java.awt.FlowLayout -import java.awt.GridLayout -import javax.swing.JButton -import javax.swing.JPanel - -class CreateSourcePanel(eocvSim: EOCVSim) : JPanel(GridLayout(2, 1)) { - - private val sourceSelectComboBox = EnumComboBox( - "", SourceType::class.java, SourceType.values(), - { it.coolName }, { SourceType.fromCoolName(it) } - ) - - private val sourceSelectPanel = JPanel(FlowLayout(FlowLayout.CENTER)) - - private val nextButton = JButton("Next") - private val nextPanel = JPanel() - - var popup: PopupX? = null - - init { - sourceSelectComboBox.removeEnumOption(SourceType.UNKNOWN) //removes the UNKNOWN enum - sourceSelectPanel.add(sourceSelectComboBox) //add to separate panel to center element - add(sourceSelectPanel) //add centered panel to this - - nextButton.addActionListener { - //creates "create source" dialog from selected enum - DialogFactory.createSourceDialog(eocvSim, sourceSelectComboBox.selectedEnum!!) - popup?.hide() - } - nextPanel.add(nextButton) - - add(nextPanel) - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.component.visualizer + +import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.gui.DialogFactory +import com.github.serivesmejia.eocvsim.gui.component.PopupX +import com.github.serivesmejia.eocvsim.gui.component.input.EnumComboBox +import com.github.serivesmejia.eocvsim.input.SourceType +import java.awt.FlowLayout +import java.awt.GridLayout +import javax.swing.JButton +import javax.swing.JPanel + +class CreateSourcePanel(eocvSim: EOCVSim) : JPanel(GridLayout(2, 1)) { + + private val sourceSelectComboBox = EnumComboBox( + "", SourceType::class.java, SourceType.values(), + { it.coolName }, { SourceType.fromCoolName(it) } + ) + + private val sourceSelectPanel = JPanel(FlowLayout(FlowLayout.CENTER)) + + private val nextButton = JButton("Next") + private val nextPanel = JPanel() + + var popup: PopupX? = null + + init { + sourceSelectComboBox.removeEnumOption(SourceType.UNKNOWN) //removes the UNKNOWN enum + sourceSelectPanel.add(sourceSelectComboBox) //add to separate panel to center element + add(sourceSelectPanel) //add centered panel to this + + nextButton.addActionListener { + //creates "create source" dialog from selected enum + DialogFactory.createSourceDialog(eocvSim, sourceSelectComboBox.selectedEnum!!) + popup?.hide() + } + nextPanel.add(nextButton) + + add(nextPanel) + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/InputSourceDropTarget.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/InputSourceDropTarget.kt index 03a85db7..9c1ee748 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/InputSourceDropTarget.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/InputSourceDropTarget.kt @@ -1,42 +1,42 @@ -package com.github.serivesmejia.eocvsim.gui.component.visualizer - -import com.github.serivesmejia.eocvsim.EOCVSim -import com.github.serivesmejia.eocvsim.gui.DialogFactory -import com.github.serivesmejia.eocvsim.input.SourceType -import com.github.serivesmejia.eocvsim.util.Log -import java.awt.datatransfer.DataFlavor -import java.awt.dnd.DnDConstants -import java.awt.dnd.DropTarget -import java.awt.dnd.DropTargetDropEvent -import java.io.File - -class InputSourceDropTarget(val eocvSim: EOCVSim) : DropTarget() { - - companion object { - private const val TAG = "InputSourceDropTarget" - } - - @Suppress("UNCHECKED_CAST") - override fun drop(evt: DropTargetDropEvent) { - try { - evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE) - val droppedFiles = evt.transferable.getTransferData( - DataFlavor.javaFileListFlavor - ) as List - - for(file in droppedFiles) { - val sourceType = SourceType.isFileUsableForSource(file) - - if(sourceType != SourceType.UNKNOWN) { - DialogFactory.createSourceDialog(eocvSim, sourceType, file) - break - } - } - } catch (e: Exception) { - Log.warn(TAG, "Drag n' drop failed", e) - } finally { - evt.dropComplete(true) - } - } - -} +package com.github.serivesmejia.eocvsim.gui.component.visualizer + +import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.gui.DialogFactory +import com.github.serivesmejia.eocvsim.input.SourceType +import com.github.serivesmejia.eocvsim.util.Log +import java.awt.datatransfer.DataFlavor +import java.awt.dnd.DnDConstants +import java.awt.dnd.DropTarget +import java.awt.dnd.DropTargetDropEvent +import java.io.File + +class InputSourceDropTarget(val eocvSim: EOCVSim) : DropTarget() { + + companion object { + private const val TAG = "InputSourceDropTarget" + } + + @Suppress("UNCHECKED_CAST") + override fun drop(evt: DropTargetDropEvent) { + try { + evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE) + val droppedFiles = evt.transferable.getTransferData( + DataFlavor.javaFileListFlavor + ) as List + + for(file in droppedFiles) { + val sourceType = SourceType.isFileUsableForSource(file) + + if(sourceType != SourceType.UNKNOWN) { + DialogFactory.createSourceDialog(eocvSim, sourceType, file) + break + } + } + } catch (e: Exception) { + Log.warn(TAG, "Drag n' drop failed", e) + } finally { + evt.dropComplete(true) + } + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/SourceSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/SourceSelectorPanel.kt index a0167cb8..12830556 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/SourceSelectorPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/SourceSelectorPanel.kt @@ -1,179 +1,179 @@ -package com.github.serivesmejia.eocvsim.gui.component.visualizer - -import com.github.serivesmejia.eocvsim.EOCVSim -import com.github.serivesmejia.eocvsim.gui.component.PopupX -import com.github.serivesmejia.eocvsim.gui.util.icon.SourcesListIconRenderer -import com.github.serivesmejia.eocvsim.pipeline.PipelineManager -import com.github.serivesmejia.eocvsim.util.extension.clipUpperZero -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.swing.Swing -import java.awt.FlowLayout -import java.awt.GridBagConstraints -import java.awt.GridBagLayout -import javax.swing.* - -class SourceSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { - - val sourceSelector = JList() - val sourceSelectorScroll = JScrollPane() - var sourceSelectorButtonsContainer = JPanel() - val sourceSelectorCreateBtt = JButton("Create") - val sourceSelectorDeleteBtt = JButton("Delete") - - val sourceSelectorLabel = JLabel("Sources") - - private var beforeSelectedSource = "" - private var beforeSelectedSourceIndex = 0 - - private var lastCreateSourcePopup: PopupX? = null - - var allowSourceSwitching = false - - init { - layout = GridBagLayout() - - sourceSelectorLabel.font = sourceSelectorLabel.font.deriveFont(20.0f) - sourceSelectorLabel.horizontalAlignment = JLabel.CENTER - - add(sourceSelectorLabel, GridBagConstraints().apply { - gridy = 0 - ipady = 20 - }) - - sourceSelector.selectionMode = ListSelectionModel.SINGLE_SELECTION - - sourceSelectorScroll.setViewportView(sourceSelector) - sourceSelectorScroll.verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_ALWAYS - sourceSelectorScroll.horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED - - add(sourceSelectorScroll, GridBagConstraints().apply { - gridy = 1 - - weightx = 0.5 - weighty = 1.0 - fill = GridBagConstraints.BOTH - - ipadx = 120 - ipady = 20 - }) - - //different icons - sourceSelector.cellRenderer = SourcesListIconRenderer(eocvSim.inputSourceManager) - - sourceSelectorCreateBtt.addActionListener { - lastCreateSourcePopup?.hide() - - val panel = CreateSourcePanel(eocvSim) - val location = sourceSelectorCreateBtt.locationOnScreen - - val frame = SwingUtilities.getWindowAncestor(this) - - val popup = PopupX(frame, panel, location.x, location.y, true) - lastCreateSourcePopup = popup - - popup.show() - } - - sourceSelectorButtonsContainer = JPanel(FlowLayout(FlowLayout.CENTER)) - - sourceSelectorButtonsContainer.add(sourceSelectorCreateBtt) - sourceSelectorButtonsContainer.add(sourceSelectorDeleteBtt) - - add(sourceSelectorButtonsContainer, GridBagConstraints().apply { - gridy = 2 - ipady = 20 - }) - - registerListeners() - } - - private fun registerListeners() { - //listener for changing input sources - sourceSelector.addListSelectionListener { evt -> - if(allowSourceSwitching && !evt.valueIsAdjusting) { - try { - if (sourceSelector.selectedIndex != -1) { - val model = sourceSelector.model - val source = model.getElementAt(sourceSelector.selectedIndex) - - //enable or disable source delete button depending if source is default or not - eocvSim.visualizer.sourceSelectorPanel.sourceSelectorDeleteBtt - .isEnabled = !(eocvSim.inputSourceManager.sources[source]?.isDefault ?: true) - - if (!evt.valueIsAdjusting && source != beforeSelectedSource) { - if (!eocvSim.pipelineManager.paused) { - eocvSim.inputSourceManager.requestSetInputSource(source) - beforeSelectedSource = source - beforeSelectedSourceIndex = sourceSelector.selectedIndex - } else { - //check if the user requested the pause or if it was due to one shoot analysis when selecting images - if (eocvSim.pipelineManager.pauseReason !== PipelineManager.PauseReason.IMAGE_ONE_ANALYSIS) { - sourceSelector.setSelectedIndex(beforeSelectedSourceIndex) - } else { //handling pausing - eocvSim.pipelineManager.requestSetPaused(false) - eocvSim.inputSourceManager.requestSetInputSource(source) - beforeSelectedSource = source - beforeSelectedSourceIndex = sourceSelector.selectedIndex - } - } - } - } else { - sourceSelector.setSelectedIndex(1) - } - } catch (ignored: ArrayIndexOutOfBoundsException) { - } - } - } - - // delete selected input source - sourceSelectorDeleteBtt.addActionListener { - val index = sourceSelector.selectedIndex - val source = sourceSelector.model.getElementAt(index) - - eocvSim.onMainUpdate.doOnce { - eocvSim.inputSourceManager.deleteInputSource(source) - updateSourcesList() - - sourceSelector.selectedIndex = (index - 1).clipUpperZero() - } - } - } - - fun updateSourcesList() = runBlocking { - launch(Dispatchers.Swing) { - val listModel = DefaultListModel() - - for (source in eocvSim.inputSourceManager.sortedInputSources) { - listModel.addElement(source.name) - } - - sourceSelector.fixedCellWidth = 240 - - sourceSelector.model = listModel - sourceSelector.revalidate() - sourceSelectorScroll.revalidate() - - sourceSelector.selectedIndex = 0 - } - } - - fun getIndexOf(name: String): Int { - for(i in 0..sourceSelector.model.size) { - if(sourceSelector.model.getElementAt(i) == name) - return i - } - - return 0 - } - - fun revalAndRepaint() { - sourceSelector.revalidate() - sourceSelector.repaint() - sourceSelectorScroll.revalidate() - sourceSelectorScroll.repaint() - revalidate(); repaint() - } - -} +package com.github.serivesmejia.eocvsim.gui.component.visualizer + +import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.gui.component.PopupX +import com.github.serivesmejia.eocvsim.gui.util.icon.SourcesListIconRenderer +import com.github.serivesmejia.eocvsim.pipeline.PipelineManager +import com.github.serivesmejia.eocvsim.util.extension.clipUpperZero +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.swing.Swing +import java.awt.FlowLayout +import java.awt.GridBagConstraints +import java.awt.GridBagLayout +import javax.swing.* + +class SourceSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { + + val sourceSelector = JList() + val sourceSelectorScroll = JScrollPane() + var sourceSelectorButtonsContainer = JPanel() + val sourceSelectorCreateBtt = JButton("Create") + val sourceSelectorDeleteBtt = JButton("Delete") + + val sourceSelectorLabel = JLabel("Sources") + + private var beforeSelectedSource = "" + private var beforeSelectedSourceIndex = 0 + + private var lastCreateSourcePopup: PopupX? = null + + var allowSourceSwitching = false + + init { + layout = GridBagLayout() + + sourceSelectorLabel.font = sourceSelectorLabel.font.deriveFont(20.0f) + sourceSelectorLabel.horizontalAlignment = JLabel.CENTER + + add(sourceSelectorLabel, GridBagConstraints().apply { + gridy = 0 + ipady = 20 + }) + + sourceSelector.selectionMode = ListSelectionModel.SINGLE_SELECTION + + sourceSelectorScroll.setViewportView(sourceSelector) + sourceSelectorScroll.verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_ALWAYS + sourceSelectorScroll.horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED + + add(sourceSelectorScroll, GridBagConstraints().apply { + gridy = 1 + + weightx = 0.5 + weighty = 1.0 + fill = GridBagConstraints.BOTH + + ipadx = 120 + ipady = 20 + }) + + //different icons + sourceSelector.cellRenderer = SourcesListIconRenderer(eocvSim.inputSourceManager) + + sourceSelectorCreateBtt.addActionListener { + lastCreateSourcePopup?.hide() + + val panel = CreateSourcePanel(eocvSim) + val location = sourceSelectorCreateBtt.locationOnScreen + + val frame = SwingUtilities.getWindowAncestor(this) + + val popup = PopupX(frame, panel, location.x, location.y, true) + lastCreateSourcePopup = popup + + popup.show() + } + + sourceSelectorButtonsContainer = JPanel(FlowLayout(FlowLayout.CENTER)) + + sourceSelectorButtonsContainer.add(sourceSelectorCreateBtt) + sourceSelectorButtonsContainer.add(sourceSelectorDeleteBtt) + + add(sourceSelectorButtonsContainer, GridBagConstraints().apply { + gridy = 2 + ipady = 20 + }) + + registerListeners() + } + + private fun registerListeners() { + //listener for changing input sources + sourceSelector.addListSelectionListener { evt -> + if(allowSourceSwitching && !evt.valueIsAdjusting) { + try { + if (sourceSelector.selectedIndex != -1) { + val model = sourceSelector.model + val source = model.getElementAt(sourceSelector.selectedIndex) + + //enable or disable source delete button depending if source is default or not + eocvSim.visualizer.sourceSelectorPanel.sourceSelectorDeleteBtt + .isEnabled = !(eocvSim.inputSourceManager.sources[source]?.isDefault ?: true) + + if (!evt.valueIsAdjusting && source != beforeSelectedSource) { + if (!eocvSim.pipelineManager.paused) { + eocvSim.inputSourceManager.requestSetInputSource(source) + beforeSelectedSource = source + beforeSelectedSourceIndex = sourceSelector.selectedIndex + } else { + //check if the user requested the pause or if it was due to one shoot analysis when selecting images + if (eocvSim.pipelineManager.pauseReason !== PipelineManager.PauseReason.IMAGE_ONE_ANALYSIS) { + sourceSelector.setSelectedIndex(beforeSelectedSourceIndex) + } else { //handling pausing + eocvSim.pipelineManager.requestSetPaused(false) + eocvSim.inputSourceManager.requestSetInputSource(source) + beforeSelectedSource = source + beforeSelectedSourceIndex = sourceSelector.selectedIndex + } + } + } + } else { + sourceSelector.setSelectedIndex(1) + } + } catch (ignored: ArrayIndexOutOfBoundsException) { + } + } + } + + // delete selected input source + sourceSelectorDeleteBtt.addActionListener { + val index = sourceSelector.selectedIndex + val source = sourceSelector.model.getElementAt(index) + + eocvSim.onMainUpdate.doOnce { + eocvSim.inputSourceManager.deleteInputSource(source) + updateSourcesList() + + sourceSelector.selectedIndex = (index - 1).clipUpperZero() + } + } + } + + fun updateSourcesList() = runBlocking { + launch(Dispatchers.Swing) { + val listModel = DefaultListModel() + + for (source in eocvSim.inputSourceManager.sortedInputSources) { + listModel.addElement(source.name) + } + + sourceSelector.fixedCellWidth = 240 + + sourceSelector.model = listModel + sourceSelector.revalidate() + sourceSelectorScroll.revalidate() + + sourceSelector.selectedIndex = 0 + } + } + + fun getIndexOf(name: String): Int { + for(i in 0..sourceSelector.model.size) { + if(sourceSelector.model.getElementAt(i) == name) + return i + } + + return 0 + } + + fun revalAndRepaint() { + sourceSelector.revalidate() + sourceSelector.repaint() + sourceSelectorScroll.revalidate() + sourceSelectorScroll.repaint() + revalidate(); repaint() + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TelemetryPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TelemetryPanel.kt index 0b314b4b..8db3f295 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TelemetryPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TelemetryPanel.kt @@ -1,100 +1,100 @@ -package com.github.serivesmejia.eocvsim.gui.component.visualizer - -import org.firstinspires.ftc.robotcore.external.Telemetry -import java.awt.FlowLayout -import java.awt.GridBagConstraints -import java.awt.GridBagLayout -import java.awt.GridLayout -import java.awt.event.MouseEvent -import java.awt.event.MouseMotionListener -import javax.swing.* - -class TelemetryPanel : JPanel() { - - val telemetryScroll = JScrollPane() - val telemetryList = JList() - - val telemetryLabel = JLabel("Telemetry") - - init { - layout = GridBagLayout() - - /* - * TELEMETRY - */ - - telemetryLabel.font = telemetryLabel.font.deriveFont(20.0f) - telemetryLabel.horizontalAlignment = JLabel.CENTER - - add(telemetryLabel, GridBagConstraints().apply { - gridy = 0 - ipady = 20 - }) - - telemetryScroll.setViewportView(telemetryList) - telemetryScroll.verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_ALWAYS - telemetryScroll.horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS - - //tooltips for the telemetry list items (thnx stackoverflow) - telemetryList.addMouseMotionListener(object : MouseMotionListener { - override fun mouseDragged(e: MouseEvent) {} - override fun mouseMoved(e: MouseEvent) { - val l = e.source as JList<*> - val m = l.model - val index = l.locationToIndex(e.point) - if (index > -1) { - l.toolTipText = m.getElementAt(index).toString() - } - } - }) - - telemetryList.selectionMode = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION - - add(telemetryScroll, GridBagConstraints().apply { - gridy = 1 - - weightx = 0.5 - weighty = 1.0 - fill = GridBagConstraints.BOTH - - ipadx = 120 - ipady = 20 - }) - } - - fun revalAndRepaint() { - telemetryList.revalidate() - telemetryList.repaint() - telemetryScroll.revalidate() - telemetryScroll.repaint() - } - - fun updateTelemetry(telemetry: Telemetry?) { - val cacheTelemetryText = telemetry.toString() - - var telemetryText: String? = null - - if (telemetry != null && telemetry.hasChanged()) { - telemetryText = cacheTelemetryText - - val listModel = DefaultListModel() - for (line in telemetryText.split("\n").toTypedArray()) { - listModel.addElement(line) - } - - telemetryList.model = listModel - } - - if (telemetryList.model.size <= 0 || (telemetryText != null && telemetryText.trim { it <= ' ' } == "")) { - val listModel = DefaultListModel() - - listModel.addElement("") - telemetryList.model = listModel - } - - telemetryList.fixedCellWidth = 240 - - revalAndRepaint() - } - -} +package com.github.serivesmejia.eocvsim.gui.component.visualizer + +import org.firstinspires.ftc.robotcore.external.Telemetry +import java.awt.FlowLayout +import java.awt.GridBagConstraints +import java.awt.GridBagLayout +import java.awt.GridLayout +import java.awt.event.MouseEvent +import java.awt.event.MouseMotionListener +import javax.swing.* + +class TelemetryPanel : JPanel() { + + val telemetryScroll = JScrollPane() + val telemetryList = JList() + + val telemetryLabel = JLabel("Telemetry") + + init { + layout = GridBagLayout() + + /* + * TELEMETRY + */ + + telemetryLabel.font = telemetryLabel.font.deriveFont(20.0f) + telemetryLabel.horizontalAlignment = JLabel.CENTER + + add(telemetryLabel, GridBagConstraints().apply { + gridy = 0 + ipady = 20 + }) + + telemetryScroll.setViewportView(telemetryList) + telemetryScroll.verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_ALWAYS + telemetryScroll.horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS + + //tooltips for the telemetry list items (thnx stackoverflow) + telemetryList.addMouseMotionListener(object : MouseMotionListener { + override fun mouseDragged(e: MouseEvent) {} + override fun mouseMoved(e: MouseEvent) { + val l = e.source as JList<*> + val m = l.model + val index = l.locationToIndex(e.point) + if (index > -1) { + l.toolTipText = m.getElementAt(index).toString() + } + } + }) + + telemetryList.selectionMode = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION + + add(telemetryScroll, GridBagConstraints().apply { + gridy = 1 + + weightx = 0.5 + weighty = 1.0 + fill = GridBagConstraints.BOTH + + ipadx = 120 + ipady = 20 + }) + } + + fun revalAndRepaint() { + telemetryList.revalidate() + telemetryList.repaint() + telemetryScroll.revalidate() + telemetryScroll.repaint() + } + + fun updateTelemetry(telemetry: Telemetry?) { + val cacheTelemetryText = telemetry.toString() + + var telemetryText: String? = null + + if (telemetry != null && telemetry.hasChanged()) { + telemetryText = cacheTelemetryText + + val listModel = DefaultListModel() + for (line in telemetryText.split("\n").toTypedArray()) { + listModel.addElement(line) + } + + telemetryList.model = listModel + } + + if (telemetryList.model.size <= 0 || (telemetryText != null && telemetryText.trim { it <= ' ' } == "")) { + val listModel = DefaultListModel() + + listModel.addElement("") + telemetryList.model = listModel + } + + telemetryList.fixedCellWidth = 240 + + revalAndRepaint() + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt index 6fe3093b..061a50ef 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt @@ -1,156 +1,156 @@ - -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.component.visualizer - -import com.github.serivesmejia.eocvsim.EOCVSim -import com.github.serivesmejia.eocvsim.gui.DialogFactory -import com.github.serivesmejia.eocvsim.gui.Visualizer -import com.github.serivesmejia.eocvsim.gui.dialog.Output -import com.github.serivesmejia.eocvsim.gui.util.GuiUtil -import com.github.serivesmejia.eocvsim.input.SourceType -import com.github.serivesmejia.eocvsim.workspace.util.VSCodeLauncher -import java.awt.Desktop -import java.net.URI -import javax.swing.JMenu -import javax.swing.JMenuBar -import javax.swing.JMenuItem - -class TopMenuBar(visualizer: Visualizer, eocvSim: EOCVSim) : JMenuBar() { - - @JvmField val mFileMenu = JMenu("File") - @JvmField val mWorkspMenu = JMenu("Workspace") - @JvmField val mEditMenu = JMenu("Edit") - @JvmField val mHelpMenu = JMenu("Help") - - @JvmField val workspCompile = JMenuItem("Build java files") - - init { - // FILE - - val fileNew = JMenu("New") - mFileMenu.add(fileNew) - - val fileNewInputSourceSubmenu = JMenu("Input Source") - fileNew.add(fileNewInputSourceSubmenu) - - //add all input source types to top bar menu - for (type in SourceType.values()) { - if (type == SourceType.UNKNOWN) continue //exclude unknown type - - val fileNewInputSourceItem = JMenuItem(type.coolName) - - fileNewInputSourceItem.addActionListener { - DialogFactory.createSourceDialog(eocvSim, type) - } - - fileNewInputSourceSubmenu.add(fileNewInputSourceItem) - } - - val fileSaveMat = JMenuItem("Save current image") - - fileSaveMat.addActionListener { - GuiUtil.saveMatFileChooser( - visualizer.frame, - visualizer.viewport.lastVisualizedMat, - eocvSim - ) - } - mFileMenu.add(fileSaveMat) - - mFileMenu.addSeparator() - - val fileRestart = JMenuItem("Restart") - - fileRestart.addActionListener { eocvSim.onMainUpdate.doOnce { eocvSim.restart() } } - mFileMenu.add(fileRestart) - - add(mFileMenu) - - //WORKSPACE - - val workspSetWorkspace = JMenuItem("Select workspace") - - workspSetWorkspace.addActionListener { visualizer.selectPipelinesWorkspace() } - mWorkspMenu.add(workspSetWorkspace) - - workspCompile.addActionListener { visualizer.asyncCompilePipelines() } - mWorkspMenu.add(workspCompile) - - val workspBuildOutput = JMenuItem("Output") - - workspBuildOutput.addActionListener { - if(!Output.isAlreadyOpened) - DialogFactory.createOutput(eocvSim, true) - } - mWorkspMenu.add(workspBuildOutput) - - mWorkspMenu.addSeparator() - - val workspVSCode = JMenu("VS Code") - - val workspVSCodeOpen = JMenuItem("Open in current workspace") - - workspVSCodeOpen.addActionListener { - VSCodeLauncher.asyncLaunch(eocvSim.workspaceManager.workspaceFile) - } - workspVSCode.add(workspVSCodeOpen) - - val workspVSCodeCreate = JMenuItem("Create VS Code workspace") - - workspVSCodeCreate.addActionListener { visualizer.createVSCodeWorkspace() } - workspVSCode.add(workspVSCodeCreate) - - mWorkspMenu.add(workspVSCode) - - add(mWorkspMenu) - - // EDIT - - val editSettings = JMenuItem("Settings") - editSettings.addActionListener { DialogFactory.createConfigDialog(eocvSim) } - - mEditMenu.add(editSettings) - add(mEditMenu) - - // HELP - - val helpUsage = JMenuItem("Usage") - helpUsage.addActionListener { - Desktop.getDesktop().browse(URI("https://github.com/deltacv/EOCV-Sim/blob/master/USAGE.md")) - } - - helpUsage.isEnabled = Desktop.isDesktopSupported() - mHelpMenu.add(helpUsage) - - mHelpMenu.addSeparator() - - val helpAbout = JMenuItem("About") - helpAbout.addActionListener { DialogFactory.createAboutDialog(eocvSim) } - - mHelpMenu.add(helpAbout) - add(mHelpMenu) - } - -} + +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.component.visualizer + +import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.gui.DialogFactory +import com.github.serivesmejia.eocvsim.gui.Visualizer +import com.github.serivesmejia.eocvsim.gui.dialog.Output +import com.github.serivesmejia.eocvsim.gui.util.GuiUtil +import com.github.serivesmejia.eocvsim.input.SourceType +import com.github.serivesmejia.eocvsim.workspace.util.VSCodeLauncher +import java.awt.Desktop +import java.net.URI +import javax.swing.JMenu +import javax.swing.JMenuBar +import javax.swing.JMenuItem + +class TopMenuBar(visualizer: Visualizer, eocvSim: EOCVSim) : JMenuBar() { + + @JvmField val mFileMenu = JMenu("File") + @JvmField val mWorkspMenu = JMenu("Workspace") + @JvmField val mEditMenu = JMenu("Edit") + @JvmField val mHelpMenu = JMenu("Help") + + @JvmField val workspCompile = JMenuItem("Build java files") + + init { + // FILE + + val fileNew = JMenu("New") + mFileMenu.add(fileNew) + + val fileNewInputSourceSubmenu = JMenu("Input Source") + fileNew.add(fileNewInputSourceSubmenu) + + //add all input source types to top bar menu + for (type in SourceType.values()) { + if (type == SourceType.UNKNOWN) continue //exclude unknown type + + val fileNewInputSourceItem = JMenuItem(type.coolName) + + fileNewInputSourceItem.addActionListener { + DialogFactory.createSourceDialog(eocvSim, type) + } + + fileNewInputSourceSubmenu.add(fileNewInputSourceItem) + } + + val fileSaveMat = JMenuItem("Save current image") + + fileSaveMat.addActionListener { + GuiUtil.saveMatFileChooser( + visualizer.frame, + visualizer.viewport.lastVisualizedMat, + eocvSim + ) + } + mFileMenu.add(fileSaveMat) + + mFileMenu.addSeparator() + + val fileRestart = JMenuItem("Restart") + + fileRestart.addActionListener { eocvSim.onMainUpdate.doOnce { eocvSim.restart() } } + mFileMenu.add(fileRestart) + + add(mFileMenu) + + //WORKSPACE + + val workspSetWorkspace = JMenuItem("Select workspace") + + workspSetWorkspace.addActionListener { visualizer.selectPipelinesWorkspace() } + mWorkspMenu.add(workspSetWorkspace) + + workspCompile.addActionListener { visualizer.asyncCompilePipelines() } + mWorkspMenu.add(workspCompile) + + val workspBuildOutput = JMenuItem("Output") + + workspBuildOutput.addActionListener { + if(!Output.isAlreadyOpened) + DialogFactory.createOutput(eocvSim, true) + } + mWorkspMenu.add(workspBuildOutput) + + mWorkspMenu.addSeparator() + + val workspVSCode = JMenu("VS Code") + + val workspVSCodeOpen = JMenuItem("Open in current workspace") + + workspVSCodeOpen.addActionListener { + VSCodeLauncher.asyncLaunch(eocvSim.workspaceManager.workspaceFile) + } + workspVSCode.add(workspVSCodeOpen) + + val workspVSCodeCreate = JMenuItem("Create VS Code workspace") + + workspVSCodeCreate.addActionListener { visualizer.createVSCodeWorkspace() } + workspVSCode.add(workspVSCodeCreate) + + mWorkspMenu.add(workspVSCode) + + add(mWorkspMenu) + + // EDIT + + val editSettings = JMenuItem("Settings") + editSettings.addActionListener { DialogFactory.createConfigDialog(eocvSim) } + + mEditMenu.add(editSettings) + add(mEditMenu) + + // HELP + + val helpUsage = JMenuItem("Usage") + helpUsage.addActionListener { + Desktop.getDesktop().browse(URI("https://github.com/deltacv/EOCV-Sim/blob/master/USAGE.md")) + } + + helpUsage.isEnabled = Desktop.isDesktopSupported() + mHelpMenu.add(helpUsage) + + mHelpMenu.addSeparator() + + val helpAbout = JMenuItem("About") + helpAbout.addActionListener { DialogFactory.createAboutDialog(eocvSim) } + + mHelpMenu.add(helpAbout) + add(mHelpMenu) + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorButtonsPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorButtonsPanel.kt index 1ec80142..11bd34e7 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorButtonsPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorButtonsPanel.kt @@ -1,110 +1,110 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline - -import com.github.serivesmejia.eocvsim.EOCVSim -import com.github.serivesmejia.eocvsim.gui.component.PopupX -import java.awt.GridBagConstraints -import java.awt.GridBagLayout -import java.awt.Insets -import javax.swing.JButton -import javax.swing.JPanel -import javax.swing.JToggleButton -import javax.swing.SwingUtilities - -class PipelineSelectorButtonsPanel(eocvSim: EOCVSim) : JPanel(GridBagLayout()) { - - val pipelinePauseBtt = JToggleButton("Pause") - val pipelineRecordBtt = JToggleButton("Record") - - val pipelineWorkspaceBtt = JButton("Workspace") - - val workspaceButtonsPanel = JPanel(GridBagLayout()) - val pipelineCompileBtt = JButton("Build java files") - - private var lastWorkspacePopup: PopupX? = null - - init { - //listener for changing pause state - pipelinePauseBtt.addActionListener { - eocvSim.onMainUpdate.doOnce { eocvSim.pipelineManager.setPaused(pipelinePauseBtt.isSelected) } - } - pipelinePauseBtt.addChangeListener { - pipelinePauseBtt.text = if(pipelinePauseBtt.isSelected) "Resume" else "Pause" - } - - add(pipelinePauseBtt, GridBagConstraints().apply { - insets = Insets(0, 0, 0, 5) - }) - - pipelineRecordBtt.addActionListener { - eocvSim.onMainUpdate.doOnce { - if (pipelineRecordBtt.isSelected) { - if (!eocvSim.isCurrentlyRecording()) eocvSim.startRecordingSession() - } else { - if (eocvSim.isCurrentlyRecording()) eocvSim.stopRecordingSession() - } - } - } - add(pipelineRecordBtt, GridBagConstraints().apply { gridx = 1 }) - - pipelineWorkspaceBtt.addActionListener { - val workspaceLocation = pipelineWorkspaceBtt.locationOnScreen - - val window = SwingUtilities.getWindowAncestor(this) - val popup = PopupX(window, workspaceButtonsPanel, workspaceLocation.x, workspaceLocation.y) - - popup.onShow { - popup.setLocation( - popup.window.location.x - workspaceButtonsPanel.width / 5, - popup.window.location.y - ) - } - - lastWorkspacePopup?.hide() - lastWorkspacePopup = popup - popup.show() - } - add(pipelineWorkspaceBtt, GridBagConstraints().apply { - gridwidth = 2 - gridy = 1 - - insets = Insets(5, 0, 0, 0) - weightx = 1.0 - - fill = GridBagConstraints.HORIZONTAL - anchor = GridBagConstraints.CENTER - }) - - // WORKSPACE BUTTONS POPUP - pipelineCompileBtt.addActionListener { eocvSim.visualizer.asyncCompilePipelines() } - workspaceButtonsPanel.add(pipelineCompileBtt, GridBagConstraints()) - - val selectWorkspBtt = JButton("Select workspace") - - selectWorkspBtt.addActionListener { eocvSim.visualizer.selectPipelinesWorkspace() } - workspaceButtonsPanel.add(selectWorkspBtt, GridBagConstraints().apply { gridx = 1 }) - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline + +import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.gui.component.PopupX +import java.awt.GridBagConstraints +import java.awt.GridBagLayout +import java.awt.Insets +import javax.swing.JButton +import javax.swing.JPanel +import javax.swing.JToggleButton +import javax.swing.SwingUtilities + +class PipelineSelectorButtonsPanel(eocvSim: EOCVSim) : JPanel(GridBagLayout()) { + + val pipelinePauseBtt = JToggleButton("Pause") + val pipelineRecordBtt = JToggleButton("Record") + + val pipelineWorkspaceBtt = JButton("Workspace") + + val workspaceButtonsPanel = JPanel(GridBagLayout()) + val pipelineCompileBtt = JButton("Build java files") + + private var lastWorkspacePopup: PopupX? = null + + init { + //listener for changing pause state + pipelinePauseBtt.addActionListener { + eocvSim.onMainUpdate.doOnce { eocvSim.pipelineManager.setPaused(pipelinePauseBtt.isSelected) } + } + pipelinePauseBtt.addChangeListener { + pipelinePauseBtt.text = if(pipelinePauseBtt.isSelected) "Resume" else "Pause" + } + + add(pipelinePauseBtt, GridBagConstraints().apply { + insets = Insets(0, 0, 0, 5) + }) + + pipelineRecordBtt.addActionListener { + eocvSim.onMainUpdate.doOnce { + if (pipelineRecordBtt.isSelected) { + if (!eocvSim.isCurrentlyRecording()) eocvSim.startRecordingSession() + } else { + if (eocvSim.isCurrentlyRecording()) eocvSim.stopRecordingSession() + } + } + } + add(pipelineRecordBtt, GridBagConstraints().apply { gridx = 1 }) + + pipelineWorkspaceBtt.addActionListener { + val workspaceLocation = pipelineWorkspaceBtt.locationOnScreen + + val window = SwingUtilities.getWindowAncestor(this) + val popup = PopupX(window, workspaceButtonsPanel, workspaceLocation.x, workspaceLocation.y) + + popup.onShow { + popup.setLocation( + popup.window.location.x - workspaceButtonsPanel.width / 5, + popup.window.location.y + ) + } + + lastWorkspacePopup?.hide() + lastWorkspacePopup = popup + popup.show() + } + add(pipelineWorkspaceBtt, GridBagConstraints().apply { + gridwidth = 2 + gridy = 1 + + insets = Insets(5, 0, 0, 0) + weightx = 1.0 + + fill = GridBagConstraints.HORIZONTAL + anchor = GridBagConstraints.CENTER + }) + + // WORKSPACE BUTTONS POPUP + pipelineCompileBtt.addActionListener { eocvSim.visualizer.asyncCompilePipelines() } + workspaceButtonsPanel.add(pipelineCompileBtt, GridBagConstraints()) + + val selectWorkspBtt = JButton("Select workspace") + + selectWorkspBtt.addActionListener { eocvSim.visualizer.selectPipelinesWorkspace() } + workspaceButtonsPanel.add(selectWorkspBtt, GridBagConstraints().apply { gridx = 1 }) + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt index b0a62702..73b2c559 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/pipeline/PipelineSelectorPanel.kt @@ -1,150 +1,150 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline - -import com.github.serivesmejia.eocvsim.EOCVSim -import com.github.serivesmejia.eocvsim.gui.util.icon.PipelineListIconRenderer -import com.github.serivesmejia.eocvsim.pipeline.PipelineManager -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.swing.Swing -import java.awt.FlowLayout -import java.awt.GridBagConstraints -import java.awt.GridBagLayout -import javax.swing.* -import javax.swing.event.ListSelectionEvent - -class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { - - var selectedIndex: Int - get() = pipelineSelector.selectedIndex - set(value) { - runBlocking { - launch(Dispatchers.Swing) { - pipelineSelector.selectedIndex = value - } - } - } - - val pipelineSelector = JList() - val pipelineSelectorScroll = JScrollPane() - - val pipelineSelectorLabel = JLabel("Pipelines") - - val buttonsPanel = PipelineSelectorButtonsPanel(eocvSim) - - var allowPipelineSwitching = false - - private var beforeSelectedPipeline = -1 - - init { - layout = GridBagLayout() - - pipelineSelectorLabel.font = pipelineSelectorLabel.font.deriveFont(20.0f) - - pipelineSelectorLabel.horizontalAlignment = JLabel.CENTER - - add(pipelineSelectorLabel, GridBagConstraints().apply { - gridy = 0 - ipady = 20 - }) - - pipelineSelector.cellRenderer = PipelineListIconRenderer(eocvSim.pipelineManager) - pipelineSelector.selectionMode = ListSelectionModel.SINGLE_SELECTION - - pipelineSelectorScroll.setViewportView(pipelineSelector) - pipelineSelectorScroll.verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_ALWAYS - pipelineSelectorScroll.horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED - - add(pipelineSelectorScroll, GridBagConstraints().apply { - gridy = 1 - - weightx = 0.5 - weighty = 1.0 - fill = GridBagConstraints.BOTH - - ipadx = 120 - ipady = 20 - }) - - add(buttonsPanel, GridBagConstraints().apply { - gridy = 2 - ipady = 20 - }) - - registerListeners() - } - - private fun registerListeners() { - - //listener for changing pipeline - pipelineSelector.addListSelectionListener { evt: ListSelectionEvent -> - if(!allowPipelineSwitching) return@addListSelectionListener - - if (pipelineSelector.selectedIndex != -1) { - val pipeline = pipelineSelector.selectedIndex - - if (!evt.valueIsAdjusting && pipeline != beforeSelectedPipeline) { - if (!eocvSim.pipelineManager.paused) { - eocvSim.pipelineManager.requestChangePipeline(pipeline) - beforeSelectedPipeline = pipeline - } else { - if (eocvSim.pipelineManager.pauseReason !== PipelineManager.PauseReason.IMAGE_ONE_ANALYSIS) { - pipelineSelector.setSelectedIndex(beforeSelectedPipeline) - } else { //handling pausing - eocvSim.pipelineManager.requestSetPaused(false) - eocvSim.pipelineManager.requestChangePipeline(pipeline) - beforeSelectedPipeline = pipeline - } - } - } - } else { - pipelineSelector.setSelectedIndex(1) - } - } - } - - fun updatePipelinesList() = runBlocking { - launch(Dispatchers.Swing) { - val listModel = DefaultListModel() - for (pipeline in eocvSim.pipelineManager.pipelines) { - listModel.addElement(pipeline.clazz.simpleName) - } - - pipelineSelector.fixedCellWidth = 240 - pipelineSelector.model = listModel - - revalAndRepaint() - } - } - - fun revalAndRepaint() { - pipelineSelector.revalidate() - pipelineSelector.repaint() - pipelineSelectorScroll.revalidate() - pipelineSelectorScroll.repaint() - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.component.visualizer.pipeline + +import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.gui.util.icon.PipelineListIconRenderer +import com.github.serivesmejia.eocvsim.pipeline.PipelineManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.swing.Swing +import java.awt.FlowLayout +import java.awt.GridBagConstraints +import java.awt.GridBagLayout +import javax.swing.* +import javax.swing.event.ListSelectionEvent + +class PipelineSelectorPanel(private val eocvSim: EOCVSim) : JPanel() { + + var selectedIndex: Int + get() = pipelineSelector.selectedIndex + set(value) { + runBlocking { + launch(Dispatchers.Swing) { + pipelineSelector.selectedIndex = value + } + } + } + + val pipelineSelector = JList() + val pipelineSelectorScroll = JScrollPane() + + val pipelineSelectorLabel = JLabel("Pipelines") + + val buttonsPanel = PipelineSelectorButtonsPanel(eocvSim) + + var allowPipelineSwitching = false + + private var beforeSelectedPipeline = -1 + + init { + layout = GridBagLayout() + + pipelineSelectorLabel.font = pipelineSelectorLabel.font.deriveFont(20.0f) + + pipelineSelectorLabel.horizontalAlignment = JLabel.CENTER + + add(pipelineSelectorLabel, GridBagConstraints().apply { + gridy = 0 + ipady = 20 + }) + + pipelineSelector.cellRenderer = PipelineListIconRenderer(eocvSim.pipelineManager) + pipelineSelector.selectionMode = ListSelectionModel.SINGLE_SELECTION + + pipelineSelectorScroll.setViewportView(pipelineSelector) + pipelineSelectorScroll.verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_ALWAYS + pipelineSelectorScroll.horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED + + add(pipelineSelectorScroll, GridBagConstraints().apply { + gridy = 1 + + weightx = 0.5 + weighty = 1.0 + fill = GridBagConstraints.BOTH + + ipadx = 120 + ipady = 20 + }) + + add(buttonsPanel, GridBagConstraints().apply { + gridy = 2 + ipady = 20 + }) + + registerListeners() + } + + private fun registerListeners() { + + //listener for changing pipeline + pipelineSelector.addListSelectionListener { evt: ListSelectionEvent -> + if(!allowPipelineSwitching) return@addListSelectionListener + + if (pipelineSelector.selectedIndex != -1) { + val pipeline = pipelineSelector.selectedIndex + + if (!evt.valueIsAdjusting && pipeline != beforeSelectedPipeline) { + if (!eocvSim.pipelineManager.paused) { + eocvSim.pipelineManager.requestChangePipeline(pipeline) + beforeSelectedPipeline = pipeline + } else { + if (eocvSim.pipelineManager.pauseReason !== PipelineManager.PauseReason.IMAGE_ONE_ANALYSIS) { + pipelineSelector.setSelectedIndex(beforeSelectedPipeline) + } else { //handling pausing + eocvSim.pipelineManager.requestSetPaused(false) + eocvSim.pipelineManager.requestChangePipeline(pipeline) + beforeSelectedPipeline = pipeline + } + } + } + } else { + pipelineSelector.setSelectedIndex(1) + } + } + } + + fun updatePipelinesList() = runBlocking { + launch(Dispatchers.Swing) { + val listModel = DefaultListModel() + for (pipeline in eocvSim.pipelineManager.pipelines) { + listModel.addElement(pipeline.clazz.simpleName) + } + + pipelineSelector.fixedCellWidth = 240 + pipelineSelector.model = listModel + + revalAndRepaint() + } + } + + fun revalAndRepaint() { + pipelineSelector.revalidate() + pipelineSelector.repaint() + pipelineSelectorScroll.revalidate() + pipelineSelectorScroll.repaint() + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/About.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/About.java index b18a523f..1b57d7e6 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/About.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/About.java @@ -1,179 +1,179 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.dialog; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.gui.Icons; -import com.github.serivesmejia.eocvsim.gui.Visualizer; -import com.github.serivesmejia.eocvsim.gui.component.ImageX; -import com.github.serivesmejia.eocvsim.gui.util.GuiUtil; -import com.github.serivesmejia.eocvsim.util.StrUtil; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import java.awt.*; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.nio.charset.StandardCharsets; - -public class About { - - public JDialog about = null; - - public static ListModel CONTRIBS_LIST_MODEL; - public static ListModel OSL_LIST_MODEL; - - static { - try { - CONTRIBS_LIST_MODEL = GuiUtil.isToListModel(About.class.getResourceAsStream("/contributors.txt"), StandardCharsets.UTF_8); - OSL_LIST_MODEL = GuiUtil.isToListModel(About.class.getResourceAsStream("/opensourcelibs.txt"), StandardCharsets.UTF_8); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - } - - public About(JFrame parent, EOCVSim eocvSim) { - - about = new JDialog(parent); - - eocvSim.visualizer.childDialogs.add(about); - initAbout(); - - } - - private void initAbout() { - - about.setModal(true); - - about.setTitle("About"); - about.setSize(445, 290); - - JPanel contents = new JPanel(new GridLayout(2, 1)); - contents.setAlignmentX(Component.CENTER_ALIGNMENT); - - ImageX icon = new ImageX(Icons.INSTANCE.getImage("ico_eocvsim")); - icon.setSize(50, 50); - icon.setAlignmentX(Component.CENTER_ALIGNMENT); - - icon.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - - JLabel appInfo = new JLabel("EasyOpenCV Simulator v" + EOCVSim.VERSION); - appInfo.setFont(appInfo.getFont().deriveFont(appInfo.getFont().getStyle() | Font.BOLD)); //set font to bold - - JPanel appInfoLogo = new JPanel(new FlowLayout()); - - appInfoLogo.add(icon); - appInfoLogo.add(appInfo); - - appInfoLogo.setBorder(BorderFactory.createEmptyBorder(10, 10, -30, 10)); - - contents.add(appInfoLogo); - - JTabbedPane tabbedPane = new JTabbedPane(); - - JPanel contributors = new JPanel(new FlowLayout(FlowLayout.CENTER)); - - JList contribsList = new JList<>(); - contribsList.setModel(CONTRIBS_LIST_MODEL); - contribsList.setSelectionModel(new GuiUtil.NoSelectionModel()); - contribsList.setLayout(new FlowLayout(FlowLayout.CENTER)); - contribsList.setAlignmentY(Component.TOP_ALIGNMENT); - - contribsList.setVisibleRowCount(4); - - JPanel contributorsList = new JPanel(new FlowLayout(FlowLayout.CENTER)); - contributorsList.setAlignmentY(Component.TOP_ALIGNMENT); - - JScrollPane contribsListScroll = new JScrollPane(); - contribsListScroll.setBorder(new EmptyBorder(0,0,20,10)); - contribsListScroll.setAlignmentX(Component.CENTER_ALIGNMENT); - contribsListScroll.setAlignmentY(Component.TOP_ALIGNMENT); - contribsListScroll.setViewportView(contribsList); - - contributors.setAlignmentY(Component.TOP_ALIGNMENT); - contents.setAlignmentY(Component.TOP_ALIGNMENT); - - contributorsList.add(contribsListScroll); - contributors.add(contributorsList); - - tabbedPane.addTab("Contributors", contributors); - - JPanel osLibs = new JPanel(new FlowLayout(FlowLayout.CENTER)); - - JList osLibsList = new JList<>(); - osLibsList.setModel(OSL_LIST_MODEL); - osLibsList.setLayout(new FlowLayout(FlowLayout.CENTER)); - osLibsList.setAlignmentY(Component.TOP_ALIGNMENT); - - osLibsList.setVisibleRowCount(4); - - osLibsList.addListSelectionListener(e -> { - if(!e.getValueIsAdjusting()) { - - String text = osLibsList.getModel().getElementAt(osLibsList.getSelectedIndex()); - String[] urls = StrUtil.findUrlsInString(text); - - if(urls.length > 0) { - try { - Desktop.getDesktop().browse(new URI(urls[0])); - } catch (Exception ex) { - ex.printStackTrace(); - } - } - - osLibsList.clearSelection(); - - } - }); - - JPanel osLibsListPane = new JPanel(new FlowLayout(FlowLayout.CENTER)); - osLibsList.setAlignmentY(Component.TOP_ALIGNMENT); - - JScrollPane osLibsListScroll = new JScrollPane(); - osLibsListScroll.setBorder(new EmptyBorder(0,0,20,10)); - osLibsListScroll.setAlignmentX(Component.CENTER_ALIGNMENT); - osLibsListScroll.setAlignmentY(Component.TOP_ALIGNMENT); - osLibsListScroll.setViewportView(osLibsList); - - osLibs.setAlignmentY(Component.TOP_ALIGNMENT); - - osLibsListPane.add(osLibsListScroll); - osLibs.add(osLibsListPane); - - tabbedPane.addTab("Open Source Libraries", osLibs); - - contents.add(tabbedPane); - - contents.setBorder(new EmptyBorder(10,10,10,10)); - - about.add(contents); - - about.setResizable(false); - - about.setLocationRelativeTo(null); - about.setVisible(true); - - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.dialog; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.gui.Icons; +import com.github.serivesmejia.eocvsim.gui.Visualizer; +import com.github.serivesmejia.eocvsim.gui.component.ImageX; +import com.github.serivesmejia.eocvsim.gui.util.GuiUtil; +import com.github.serivesmejia.eocvsim.util.StrUtil; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.nio.charset.StandardCharsets; + +public class About { + + public JDialog about = null; + + public static ListModel CONTRIBS_LIST_MODEL; + public static ListModel OSL_LIST_MODEL; + + static { + try { + CONTRIBS_LIST_MODEL = GuiUtil.isToListModel(About.class.getResourceAsStream("/contributors.txt"), StandardCharsets.UTF_8); + OSL_LIST_MODEL = GuiUtil.isToListModel(About.class.getResourceAsStream("/opensourcelibs.txt"), StandardCharsets.UTF_8); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + } + + public About(JFrame parent, EOCVSim eocvSim) { + + about = new JDialog(parent); + + eocvSim.visualizer.childDialogs.add(about); + initAbout(); + + } + + private void initAbout() { + + about.setModal(true); + + about.setTitle("About"); + about.setSize(445, 290); + + JPanel contents = new JPanel(new GridLayout(2, 1)); + contents.setAlignmentX(Component.CENTER_ALIGNMENT); + + ImageX icon = new ImageX(Icons.INSTANCE.getImage("ico_eocvsim")); + icon.setSize(50, 50); + icon.setAlignmentX(Component.CENTER_ALIGNMENT); + + icon.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + JLabel appInfo = new JLabel("EasyOpenCV Simulator v" + EOCVSim.VERSION); + appInfo.setFont(appInfo.getFont().deriveFont(appInfo.getFont().getStyle() | Font.BOLD)); //set font to bold + + JPanel appInfoLogo = new JPanel(new FlowLayout()); + + appInfoLogo.add(icon); + appInfoLogo.add(appInfo); + + appInfoLogo.setBorder(BorderFactory.createEmptyBorder(10, 10, -30, 10)); + + contents.add(appInfoLogo); + + JTabbedPane tabbedPane = new JTabbedPane(); + + JPanel contributors = new JPanel(new FlowLayout(FlowLayout.CENTER)); + + JList contribsList = new JList<>(); + contribsList.setModel(CONTRIBS_LIST_MODEL); + contribsList.setSelectionModel(new GuiUtil.NoSelectionModel()); + contribsList.setLayout(new FlowLayout(FlowLayout.CENTER)); + contribsList.setAlignmentY(Component.TOP_ALIGNMENT); + + contribsList.setVisibleRowCount(4); + + JPanel contributorsList = new JPanel(new FlowLayout(FlowLayout.CENTER)); + contributorsList.setAlignmentY(Component.TOP_ALIGNMENT); + + JScrollPane contribsListScroll = new JScrollPane(); + contribsListScroll.setBorder(new EmptyBorder(0,0,20,10)); + contribsListScroll.setAlignmentX(Component.CENTER_ALIGNMENT); + contribsListScroll.setAlignmentY(Component.TOP_ALIGNMENT); + contribsListScroll.setViewportView(contribsList); + + contributors.setAlignmentY(Component.TOP_ALIGNMENT); + contents.setAlignmentY(Component.TOP_ALIGNMENT); + + contributorsList.add(contribsListScroll); + contributors.add(contributorsList); + + tabbedPane.addTab("Contributors", contributors); + + JPanel osLibs = new JPanel(new FlowLayout(FlowLayout.CENTER)); + + JList osLibsList = new JList<>(); + osLibsList.setModel(OSL_LIST_MODEL); + osLibsList.setLayout(new FlowLayout(FlowLayout.CENTER)); + osLibsList.setAlignmentY(Component.TOP_ALIGNMENT); + + osLibsList.setVisibleRowCount(4); + + osLibsList.addListSelectionListener(e -> { + if(!e.getValueIsAdjusting()) { + + String text = osLibsList.getModel().getElementAt(osLibsList.getSelectedIndex()); + String[] urls = StrUtil.findUrlsInString(text); + + if(urls.length > 0) { + try { + Desktop.getDesktop().browse(new URI(urls[0])); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + osLibsList.clearSelection(); + + } + }); + + JPanel osLibsListPane = new JPanel(new FlowLayout(FlowLayout.CENTER)); + osLibsList.setAlignmentY(Component.TOP_ALIGNMENT); + + JScrollPane osLibsListScroll = new JScrollPane(); + osLibsListScroll.setBorder(new EmptyBorder(0,0,20,10)); + osLibsListScroll.setAlignmentX(Component.CENTER_ALIGNMENT); + osLibsListScroll.setAlignmentY(Component.TOP_ALIGNMENT); + osLibsListScroll.setViewportView(osLibsList); + + osLibs.setAlignmentY(Component.TOP_ALIGNMENT); + + osLibsListPane.add(osLibsListScroll); + osLibs.add(osLibsListPane); + + tabbedPane.addTab("Open Source Libraries", osLibs); + + contents.add(tabbedPane); + + contents.setBorder(new EmptyBorder(10,10,10,10)); + + about.add(contents); + + about.setResizable(false); + + about.setLocationRelativeTo(null); + about.setVisible(true); + + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/Configuration.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/Configuration.java index f8883d67..301d06cc 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/Configuration.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/Configuration.java @@ -1,169 +1,169 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.dialog; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.config.Config; -import com.github.serivesmejia.eocvsim.gui.component.input.EnumComboBox; -import com.github.serivesmejia.eocvsim.gui.component.input.SizeFields; -import com.github.serivesmejia.eocvsim.gui.theme.Theme; -import com.github.serivesmejia.eocvsim.pipeline.PipelineFps; -import com.github.serivesmejia.eocvsim.pipeline.PipelineTimeout; - -import javax.swing.*; -import java.awt.*; - -public class Configuration { - - private final EOCVSim eocvSim; - public JPanel contents = new JPanel(new GridLayout(7, 1, 1, 8)); - public JComboBox themeComboBox = new JComboBox<>(); - - public JButton acceptButton = new JButton("Accept"); - - public JCheckBox pauseOnImageCheckBox = new JCheckBox(); - - public EnumComboBox pipelineTimeoutComboBox = null; - public EnumComboBox pipelineFpsComboBox = null; - - public SizeFields videoRecordingSize = null; - public EnumComboBox videoRecordingFpsComboBox = null; - - JDialog configuration; - - public Configuration(JFrame parent, EOCVSim eocvSim) { - configuration = new JDialog(parent); - this.eocvSim = eocvSim; - - eocvSim.visualizer.childDialogs.add(configuration); - - initConfiguration(); - } - - private void initConfiguration() { - - Config config = this.eocvSim.configManager.getConfig(); - configuration.setModal(true); - configuration.setTitle("Settings"); - configuration.setSize(350, 320); - - JPanel themePanel = new JPanel(new FlowLayout()); - JLabel themeLabel = new JLabel("Theme: "); - - themeLabel.setHorizontalAlignment(0); - - for (Theme theme : Theme.values()) { - this.themeComboBox.addItem(theme.toString().replace("_", " ")); - } - - themeComboBox.setSelectedIndex(eocvSim.getConfig().simTheme.ordinal()); - themePanel.add(themeLabel); - themePanel.add(this.themeComboBox); - contents.add(themePanel); - - JPanel pauseOnImagePanel = new JPanel(new FlowLayout()); - JLabel pauseOnImageLabel = new JLabel("Pause with Image Sources"); - - pauseOnImageCheckBox.setSelected(config.pauseOnImages); - - pauseOnImagePanel.add(pauseOnImageCheckBox); - pauseOnImagePanel.add(pauseOnImageLabel); - - contents.add(pauseOnImagePanel); - - pipelineTimeoutComboBox = new EnumComboBox<>( - "Pipeline Process Timeout: ", PipelineTimeout.class, - PipelineTimeout.values(), PipelineTimeout::getCoolName, PipelineTimeout::fromCoolName - ); - pipelineTimeoutComboBox.setSelectedEnum(config.pipelineTimeout); - contents.add(pipelineTimeoutComboBox); - - pipelineFpsComboBox = new EnumComboBox<>( - "Pipeline Max FPS: ", PipelineFps.class, - PipelineFps.values(), PipelineFps::getCoolName, PipelineFps::fromCoolName - ); - pipelineFpsComboBox.setSelectedEnum(config.pipelineMaxFps); - contents.add(this.pipelineFpsComboBox); - - videoRecordingSize = new SizeFields( - config.videoRecordingSize, false, - "Video Recording Size: " - ); - videoRecordingSize.onChange.doPersistent(() -> - acceptButton.setEnabled(this.videoRecordingSize.getValid()) - ); - contents.add(this.videoRecordingSize); - - // video fps - - videoRecordingFpsComboBox = new EnumComboBox<>( - "Video Recording FPS: ", PipelineFps.class, - PipelineFps.values(), PipelineFps::getCoolName, PipelineFps::fromCoolName - ); - videoRecordingFpsComboBox.setSelectedEnum(config.videoRecordingFps); - contents.add(videoRecordingFpsComboBox); - - JPanel acceptPanel = new JPanel(new FlowLayout()); - acceptPanel.add(acceptButton); - - acceptButton.addActionListener(e -> { - this.eocvSim.onMainUpdate.doOnce(this::applyChanges); - close(); - }); - - contents.add(acceptPanel); - contents.setBorder(BorderFactory.createEmptyBorder(10, 0, 10, 0)); - - configuration.add(this.contents); - configuration.setResizable(false); - configuration.setLocationRelativeTo(null); - configuration.setVisible(true); - } - - private void applyChanges() { - Config config = eocvSim.configManager.getConfig(); - - Theme userSelectedTheme = Theme.valueOf(themeComboBox.getSelectedItem().toString().replace(" ", "_")); - Theme beforeTheme = config.simTheme; - - //save user modifications to config - config.simTheme = userSelectedTheme; - config.pauseOnImages = pauseOnImageCheckBox.isSelected(); - config.pipelineTimeout = pipelineTimeoutComboBox.getSelectedEnum(); - config.pipelineMaxFps = pipelineFpsComboBox.getSelectedEnum(); - config.videoRecordingSize = videoRecordingSize.getCurrentSize(); - config.videoRecordingFps = videoRecordingFpsComboBox.getSelectedEnum(); - - eocvSim.configManager.saveToFile(); //update config file - - if (userSelectedTheme != beforeTheme) - eocvSim.restart(); - } - - public void close() { - configuration.setVisible(false); - configuration.dispose(); - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.dialog; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.config.Config; +import com.github.serivesmejia.eocvsim.gui.component.input.EnumComboBox; +import com.github.serivesmejia.eocvsim.gui.component.input.SizeFields; +import com.github.serivesmejia.eocvsim.gui.theme.Theme; +import com.github.serivesmejia.eocvsim.pipeline.PipelineFps; +import com.github.serivesmejia.eocvsim.pipeline.PipelineTimeout; + +import javax.swing.*; +import java.awt.*; + +public class Configuration { + + private final EOCVSim eocvSim; + public JPanel contents = new JPanel(new GridLayout(7, 1, 1, 8)); + public JComboBox themeComboBox = new JComboBox<>(); + + public JButton acceptButton = new JButton("Accept"); + + public JCheckBox pauseOnImageCheckBox = new JCheckBox(); + + public EnumComboBox pipelineTimeoutComboBox = null; + public EnumComboBox pipelineFpsComboBox = null; + + public SizeFields videoRecordingSize = null; + public EnumComboBox videoRecordingFpsComboBox = null; + + JDialog configuration; + + public Configuration(JFrame parent, EOCVSim eocvSim) { + configuration = new JDialog(parent); + this.eocvSim = eocvSim; + + eocvSim.visualizer.childDialogs.add(configuration); + + initConfiguration(); + } + + private void initConfiguration() { + + Config config = this.eocvSim.configManager.getConfig(); + configuration.setModal(true); + configuration.setTitle("Settings"); + configuration.setSize(350, 320); + + JPanel themePanel = new JPanel(new FlowLayout()); + JLabel themeLabel = new JLabel("Theme: "); + + themeLabel.setHorizontalAlignment(0); + + for (Theme theme : Theme.values()) { + this.themeComboBox.addItem(theme.toString().replace("_", " ")); + } + + themeComboBox.setSelectedIndex(eocvSim.getConfig().simTheme.ordinal()); + themePanel.add(themeLabel); + themePanel.add(this.themeComboBox); + contents.add(themePanel); + + JPanel pauseOnImagePanel = new JPanel(new FlowLayout()); + JLabel pauseOnImageLabel = new JLabel("Pause with Image Sources"); + + pauseOnImageCheckBox.setSelected(config.pauseOnImages); + + pauseOnImagePanel.add(pauseOnImageCheckBox); + pauseOnImagePanel.add(pauseOnImageLabel); + + contents.add(pauseOnImagePanel); + + pipelineTimeoutComboBox = new EnumComboBox<>( + "Pipeline Process Timeout: ", PipelineTimeout.class, + PipelineTimeout.values(), PipelineTimeout::getCoolName, PipelineTimeout::fromCoolName + ); + pipelineTimeoutComboBox.setSelectedEnum(config.pipelineTimeout); + contents.add(pipelineTimeoutComboBox); + + pipelineFpsComboBox = new EnumComboBox<>( + "Pipeline Max FPS: ", PipelineFps.class, + PipelineFps.values(), PipelineFps::getCoolName, PipelineFps::fromCoolName + ); + pipelineFpsComboBox.setSelectedEnum(config.pipelineMaxFps); + contents.add(this.pipelineFpsComboBox); + + videoRecordingSize = new SizeFields( + config.videoRecordingSize, false, + "Video Recording Size: " + ); + videoRecordingSize.onChange.doPersistent(() -> + acceptButton.setEnabled(this.videoRecordingSize.getValid()) + ); + contents.add(this.videoRecordingSize); + + // video fps + + videoRecordingFpsComboBox = new EnumComboBox<>( + "Video Recording FPS: ", PipelineFps.class, + PipelineFps.values(), PipelineFps::getCoolName, PipelineFps::fromCoolName + ); + videoRecordingFpsComboBox.setSelectedEnum(config.videoRecordingFps); + contents.add(videoRecordingFpsComboBox); + + JPanel acceptPanel = new JPanel(new FlowLayout()); + acceptPanel.add(acceptButton); + + acceptButton.addActionListener(e -> { + this.eocvSim.onMainUpdate.doOnce(this::applyChanges); + close(); + }); + + contents.add(acceptPanel); + contents.setBorder(BorderFactory.createEmptyBorder(10, 0, 10, 0)); + + configuration.add(this.contents); + configuration.setResizable(false); + configuration.setLocationRelativeTo(null); + configuration.setVisible(true); + } + + private void applyChanges() { + Config config = eocvSim.configManager.getConfig(); + + Theme userSelectedTheme = Theme.valueOf(themeComboBox.getSelectedItem().toString().replace(" ", "_")); + Theme beforeTheme = config.simTheme; + + //save user modifications to config + config.simTheme = userSelectedTheme; + config.pauseOnImages = pauseOnImageCheckBox.isSelected(); + config.pipelineTimeout = pipelineTimeoutComboBox.getSelectedEnum(); + config.pipelineMaxFps = pipelineFpsComboBox.getSelectedEnum(); + config.videoRecordingSize = videoRecordingSize.getCurrentSize(); + config.videoRecordingFps = videoRecordingFpsComboBox.getSelectedEnum(); + + eocvSim.configManager.saveToFile(); //update config file + + if (userSelectedTheme != beforeTheme) + eocvSim.restart(); + } + + public void close() { + configuration.setVisible(false); + configuration.dispose(); + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/FileAlreadyExists.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/FileAlreadyExists.java index df7867b0..a5565fb1 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/FileAlreadyExists.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/FileAlreadyExists.java @@ -1,97 +1,97 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.dialog; - -import com.github.serivesmejia.eocvsim.EOCVSim; - -import javax.swing.*; -import java.awt.*; - -public class FileAlreadyExists { - - public static volatile boolean alreadyOpened = false; - public volatile JDialog fileAlreadyExists = null; - public volatile JPanel contentsPanel = new JPanel(); - public volatile UserChoice userChoice; - EOCVSim eocvSim = null; - - public FileAlreadyExists(JFrame parent, EOCVSim eocvSim) { - - fileAlreadyExists = new JDialog(parent); - - this.eocvSim = eocvSim; - - eocvSim.visualizer.childDialogs.add(fileAlreadyExists); - - } - - public UserChoice run() { - - fileAlreadyExists.setModal(true); - - fileAlreadyExists.setTitle("Warning"); - fileAlreadyExists.setSize(300, 120); - - JPanel alreadyExistsPanel = new JPanel(new FlowLayout()); - - JLabel alreadyExistsLabel = new JLabel("File already exists in the selected directory"); - alreadyExistsPanel.add(alreadyExistsLabel); - - contentsPanel.add(alreadyExistsPanel); - - JPanel replaceCancelPanel = new JPanel(new FlowLayout()); - - JButton replaceButton = new JButton("Replace"); - replaceCancelPanel.add(replaceButton); - - replaceButton.addActionListener((e) -> { - userChoice = UserChoice.REPLACE; - fileAlreadyExists.setVisible(false); - }); - - JButton cancelButton = new JButton("Cancel"); - replaceCancelPanel.add(cancelButton); - - cancelButton.addActionListener((e) -> { - userChoice = UserChoice.CANCEL; - fileAlreadyExists.setVisible(false); - }); - - contentsPanel.add(replaceCancelPanel); - - fileAlreadyExists.add(contentsPanel); - - fileAlreadyExists.setResizable(false); - fileAlreadyExists.setLocationRelativeTo(null); - fileAlreadyExists.setVisible(true); - - while (userChoice == UserChoice.NA) ; - - return userChoice; - - } - - public enum UserChoice {NA, REPLACE, CANCEL} - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.dialog; + +import com.github.serivesmejia.eocvsim.EOCVSim; + +import javax.swing.*; +import java.awt.*; + +public class FileAlreadyExists { + + public static volatile boolean alreadyOpened = false; + public volatile JDialog fileAlreadyExists = null; + public volatile JPanel contentsPanel = new JPanel(); + public volatile UserChoice userChoice; + EOCVSim eocvSim = null; + + public FileAlreadyExists(JFrame parent, EOCVSim eocvSim) { + + fileAlreadyExists = new JDialog(parent); + + this.eocvSim = eocvSim; + + eocvSim.visualizer.childDialogs.add(fileAlreadyExists); + + } + + public UserChoice run() { + + fileAlreadyExists.setModal(true); + + fileAlreadyExists.setTitle("Warning"); + fileAlreadyExists.setSize(300, 120); + + JPanel alreadyExistsPanel = new JPanel(new FlowLayout()); + + JLabel alreadyExistsLabel = new JLabel("File already exists in the selected directory"); + alreadyExistsPanel.add(alreadyExistsLabel); + + contentsPanel.add(alreadyExistsPanel); + + JPanel replaceCancelPanel = new JPanel(new FlowLayout()); + + JButton replaceButton = new JButton("Replace"); + replaceCancelPanel.add(replaceButton); + + replaceButton.addActionListener((e) -> { + userChoice = UserChoice.REPLACE; + fileAlreadyExists.setVisible(false); + }); + + JButton cancelButton = new JButton("Cancel"); + replaceCancelPanel.add(cancelButton); + + cancelButton.addActionListener((e) -> { + userChoice = UserChoice.CANCEL; + fileAlreadyExists.setVisible(false); + }); + + contentsPanel.add(replaceCancelPanel); + + fileAlreadyExists.add(contentsPanel); + + fileAlreadyExists.setResizable(false); + fileAlreadyExists.setLocationRelativeTo(null); + fileAlreadyExists.setVisible(true); + + while (userChoice == UserChoice.NA) ; + + return userChoice; + + } + + public enum UserChoice {NA, REPLACE, CANCEL} + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/Output.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/Output.kt index c58ca77c..d28b01ef 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/Output.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/Output.kt @@ -1,239 +1,239 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.dialog - -import com.github.serivesmejia.eocvsim.EOCVSim -import com.github.serivesmejia.eocvsim.gui.dialog.component.OutputPanel -import com.github.serivesmejia.eocvsim.pipeline.compiler.PipelineCompileStatus -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.swing.Swing -import java.awt.Dimension -import java.awt.event.WindowAdapter -import java.awt.event.WindowEvent -import javax.swing.* - -class Output @JvmOverloads constructor( - parent: JFrame, - private val eocvSim: EOCVSim, - private val tabbedPaneIndex: Int = latestIndex, - private val wasManuallyOpened: Boolean = false -) { - - companion object { - var isAlreadyOpened = false - private set - - var latestIndex = 0 - private set - } - - private val output = JDialog(parent) - - private val buildBottomButtonsPanel = BuildOutputBottomButtonsPanel(::close) - private val buildOutputPanel = OutputPanel(buildBottomButtonsPanel) - - private val pipelineBottomButtonsPanel = PipelineBottomButtonsPanel(::close) - private val pipelineOutputPanel = OutputPanel(pipelineBottomButtonsPanel) - - private val tabbedPane = JTabbedPane() - - private val compiledPipelineManager = eocvSim.pipelineManager.compiledPipelineManager - private val pipelineExceptionTracker = eocvSim.pipelineManager.pipelineExceptionTracker - - init { - isAlreadyOpened = true - - eocvSim.visualizer.childDialogs.add(output) - - output.isModal = true - output.title = "Output" - output.setSize(500, 350) - - tabbedPane.add("Pipeline Output", pipelineOutputPanel) - tabbedPane.add("Build Output", buildOutputPanel) - - tabbedPane.selectedIndex = tabbedPaneIndex - - output.contentPane.add(tabbedPane) - - output.setLocationRelativeTo(null) - - updatePipelineOutput() - - if(eocvSim.pipelineManager.paused) { - pipelinePaused() - } else { - pipelineResumed() - } - - buildEnded(true) - if(compiledPipelineManager.isBuildRunning) { - buildRunning() - } - - registerListeners() - - output.isVisible = true - } - - private fun registerListeners() = GlobalScope.launch(Dispatchers.Swing) { - output.addWindowListener(object: WindowAdapter() { - override fun windowClosing(e: WindowEvent) { - close() - } - }) - - pipelineExceptionTracker.onUpdate { - if(!output.isVisible) { - it.removeThis() - } else { - updatePipelineOutput() - } - } - - compiledPipelineManager.onBuildStart { - if(!output.isVisible) { - it.removeThis() - } else { - buildRunning() - } - } - - compiledPipelineManager.onBuildEnd { - if(!output.isVisible) { - it.removeThis() - } else { - buildEnded() - tabbedPane.selectedIndex = 1 - } - } - - eocvSim.pipelineManager.onPause { - if(!output.isVisible) { - it.removeThis() - } else { - pipelinePaused() - } - } - - eocvSim.pipelineManager.onResume { - if(!output.isVisible) { - it.removeThis() - } else { - pipelineResumed() - } - } - - pipelineBottomButtonsPanel.pauseButton.addActionListener { - eocvSim.pipelineManager.setPaused(pipelineBottomButtonsPanel.pauseButton.isSelected) - - if(pipelineBottomButtonsPanel.pauseButton.isSelected) { - pipelinePaused() - } else { - pipelineResumed() - } - } - - pipelineBottomButtonsPanel.clearButton.addActionListener { - eocvSim.pipelineManager.pipelineExceptionTracker.clear() - } - - buildBottomButtonsPanel.buildAgainButton.addActionListener { - eocvSim.visualizer.asyncCompilePipelines() - } - } - - private fun updatePipelineOutput() { - pipelineOutputPanel.outputArea.text = pipelineExceptionTracker.message - } - - private fun buildRunning() { - buildBottomButtonsPanel.buildAgainButton.isEnabled = false - buildOutputPanel.outputArea.text = "Build running..." - } - - private fun buildEnded(calledOnInit: Boolean = false) { - compiledPipelineManager.run { - if(!wasManuallyOpened && - compiledPipelineManager.lastBuildResult!!.status == PipelineCompileStatus.SUCCESS && - tabbedPaneIndex == 1 && !calledOnInit - ) { - // close if the dialog was automatically opened in the - // "build output" tab and a new build was successful - close() - return@buildEnded - } - - buildOutputPanel.outputArea.text = when { - lastBuildOutputMessage != null -> lastBuildOutputMessage!! - lastBuildResult != null -> lastBuildResult!!.message - else -> "No output" - } - } - - buildBottomButtonsPanel.buildAgainButton.isEnabled = true - } - - private fun pipelineResumed() { - pipelineBottomButtonsPanel.pauseButton.isSelected = false - pipelineBottomButtonsPanel.pauseButton.text = "Pause" - } - - private fun pipelinePaused() { - pipelineBottomButtonsPanel.pauseButton.isSelected = true - pipelineBottomButtonsPanel.pauseButton.text = "Resume" - } - - fun close() { - output.isVisible = false - isAlreadyOpened = false - latestIndex = tabbedPane.selectedIndex - } - - class PipelineBottomButtonsPanel( - closeCallback: () -> Unit - ) : OutputPanel.DefaultBottomButtonsPanel(closeCallback) { - val pauseButton = JToggleButton("Pause") - - override fun create(panel: OutputPanel) { - add(Box.createRigidArea(Dimension(4, 0))) - add(pauseButton) - super.create(panel) - } - } - - class BuildOutputBottomButtonsPanel( - closeCallback: () -> Unit, - ) : OutputPanel.DefaultBottomButtonsPanel(closeCallback) { - val buildAgainButton = JButton("Build again") - - override fun create(panel: OutputPanel) { - add(Box.createRigidArea(Dimension(4, 0))) - add(buildAgainButton) - super.create(panel) - } - } -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.dialog + +import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.gui.dialog.component.OutputPanel +import com.github.serivesmejia.eocvsim.pipeline.compiler.PipelineCompileStatus +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.swing.Swing +import java.awt.Dimension +import java.awt.event.WindowAdapter +import java.awt.event.WindowEvent +import javax.swing.* + +class Output @JvmOverloads constructor( + parent: JFrame, + private val eocvSim: EOCVSim, + private val tabbedPaneIndex: Int = latestIndex, + private val wasManuallyOpened: Boolean = false +) { + + companion object { + var isAlreadyOpened = false + private set + + var latestIndex = 0 + private set + } + + private val output = JDialog(parent) + + private val buildBottomButtonsPanel = BuildOutputBottomButtonsPanel(::close) + private val buildOutputPanel = OutputPanel(buildBottomButtonsPanel) + + private val pipelineBottomButtonsPanel = PipelineBottomButtonsPanel(::close) + private val pipelineOutputPanel = OutputPanel(pipelineBottomButtonsPanel) + + private val tabbedPane = JTabbedPane() + + private val compiledPipelineManager = eocvSim.pipelineManager.compiledPipelineManager + private val pipelineExceptionTracker = eocvSim.pipelineManager.pipelineExceptionTracker + + init { + isAlreadyOpened = true + + eocvSim.visualizer.childDialogs.add(output) + + output.isModal = true + output.title = "Output" + output.setSize(500, 350) + + tabbedPane.add("Pipeline Output", pipelineOutputPanel) + tabbedPane.add("Build Output", buildOutputPanel) + + tabbedPane.selectedIndex = tabbedPaneIndex + + output.contentPane.add(tabbedPane) + + output.setLocationRelativeTo(null) + + updatePipelineOutput() + + if(eocvSim.pipelineManager.paused) { + pipelinePaused() + } else { + pipelineResumed() + } + + buildEnded(true) + if(compiledPipelineManager.isBuildRunning) { + buildRunning() + } + + registerListeners() + + output.isVisible = true + } + + private fun registerListeners() = GlobalScope.launch(Dispatchers.Swing) { + output.addWindowListener(object: WindowAdapter() { + override fun windowClosing(e: WindowEvent) { + close() + } + }) + + pipelineExceptionTracker.onUpdate { + if(!output.isVisible) { + it.removeThis() + } else { + updatePipelineOutput() + } + } + + compiledPipelineManager.onBuildStart { + if(!output.isVisible) { + it.removeThis() + } else { + buildRunning() + } + } + + compiledPipelineManager.onBuildEnd { + if(!output.isVisible) { + it.removeThis() + } else { + buildEnded() + tabbedPane.selectedIndex = 1 + } + } + + eocvSim.pipelineManager.onPause { + if(!output.isVisible) { + it.removeThis() + } else { + pipelinePaused() + } + } + + eocvSim.pipelineManager.onResume { + if(!output.isVisible) { + it.removeThis() + } else { + pipelineResumed() + } + } + + pipelineBottomButtonsPanel.pauseButton.addActionListener { + eocvSim.pipelineManager.setPaused(pipelineBottomButtonsPanel.pauseButton.isSelected) + + if(pipelineBottomButtonsPanel.pauseButton.isSelected) { + pipelinePaused() + } else { + pipelineResumed() + } + } + + pipelineBottomButtonsPanel.clearButton.addActionListener { + eocvSim.pipelineManager.pipelineExceptionTracker.clear() + } + + buildBottomButtonsPanel.buildAgainButton.addActionListener { + eocvSim.visualizer.asyncCompilePipelines() + } + } + + private fun updatePipelineOutput() { + pipelineOutputPanel.outputArea.text = pipelineExceptionTracker.message + } + + private fun buildRunning() { + buildBottomButtonsPanel.buildAgainButton.isEnabled = false + buildOutputPanel.outputArea.text = "Build running..." + } + + private fun buildEnded(calledOnInit: Boolean = false) { + compiledPipelineManager.run { + if(!wasManuallyOpened && + compiledPipelineManager.lastBuildResult!!.status == PipelineCompileStatus.SUCCESS && + tabbedPaneIndex == 1 && !calledOnInit + ) { + // close if the dialog was automatically opened in the + // "build output" tab and a new build was successful + close() + return@buildEnded + } + + buildOutputPanel.outputArea.text = when { + lastBuildOutputMessage != null -> lastBuildOutputMessage!! + lastBuildResult != null -> lastBuildResult!!.message + else -> "No output" + } + } + + buildBottomButtonsPanel.buildAgainButton.isEnabled = true + } + + private fun pipelineResumed() { + pipelineBottomButtonsPanel.pauseButton.isSelected = false + pipelineBottomButtonsPanel.pauseButton.text = "Pause" + } + + private fun pipelinePaused() { + pipelineBottomButtonsPanel.pauseButton.isSelected = true + pipelineBottomButtonsPanel.pauseButton.text = "Resume" + } + + fun close() { + output.isVisible = false + isAlreadyOpened = false + latestIndex = tabbedPane.selectedIndex + } + + class PipelineBottomButtonsPanel( + closeCallback: () -> Unit + ) : OutputPanel.DefaultBottomButtonsPanel(closeCallback) { + val pauseButton = JToggleButton("Pause") + + override fun create(panel: OutputPanel) { + add(Box.createRigidArea(Dimension(4, 0))) + add(pauseButton) + super.create(panel) + } + } + + class BuildOutputBottomButtonsPanel( + closeCallback: () -> Unit, + ) : OutputPanel.DefaultBottomButtonsPanel(closeCallback) { + val buildAgainButton = JButton("Build again") + + override fun create(panel: OutputPanel) { + add(Box.createRigidArea(Dimension(4, 0))) + add(buildAgainButton) + super.create(panel) + } + } +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/SplashScreen.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/SplashScreen.kt index 3161311e..3987a033 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/SplashScreen.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/SplashScreen.kt @@ -1,57 +1,57 @@ -package com.github.serivesmejia.eocvsim.gui.dialog - -import com.github.serivesmejia.eocvsim.gui.Icons -import com.github.serivesmejia.eocvsim.util.event.EventHandler -import java.awt.* -import javax.swing.JDialog -import javax.swing.JPanel -import javax.swing.SwingUtilities - -class SplashScreen(closeHandler: EventHandler? = null) : JDialog() { - - init { - if(closeHandler != null) { - closeHandler { - SwingUtilities.invokeLater { - isVisible = false - dispose() - } - } - } - - val image = ImagePanel() - add(image) - - setLocationRelativeTo(null) - isUndecorated = true - isAlwaysOnTop = true - background = Color(0, 0, 0, 0) - - cursor = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR) - - pack() - - val screenSize = Toolkit.getDefaultToolkit().screenSize - val x = (screenSize.width - image.width) / 2 - val y = (screenSize.height - image.height) / 2 - - setLocation(x, y) - isVisible = true - } - - class ImagePanel : JPanel(GridBagLayout()) { - val img = Icons.getImage("ico_eocvsim") - - init { - isOpaque = false - } - - override fun paintComponent(g: Graphics) { - super.paintComponent(g) - g.drawImage(img.image, 0, 0, width, height, this) - } - - override fun getPreferredSize() = Dimension(img.iconWidth / 2, img.iconHeight / 2) - } - +package com.github.serivesmejia.eocvsim.gui.dialog + +import com.github.serivesmejia.eocvsim.gui.Icons +import com.github.serivesmejia.eocvsim.util.event.EventHandler +import java.awt.* +import javax.swing.JDialog +import javax.swing.JPanel +import javax.swing.SwingUtilities + +class SplashScreen(closeHandler: EventHandler? = null) : JDialog() { + + init { + if(closeHandler != null) { + closeHandler { + SwingUtilities.invokeLater { + isVisible = false + dispose() + } + } + } + + val image = ImagePanel() + add(image) + + setLocationRelativeTo(null) + isUndecorated = true + isAlwaysOnTop = true + background = Color(0, 0, 0, 0) + + cursor = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR) + + pack() + + val screenSize = Toolkit.getDefaultToolkit().screenSize + val x = (screenSize.width - image.width) / 2 + val y = (screenSize.height - image.height) / 2 + + setLocation(x, y) + isVisible = true + } + + class ImagePanel : JPanel(GridBagLayout()) { + val img = Icons.getImage("ico_eocvsim") + + init { + isOpaque = false + } + + override fun paintComponent(g: Graphics) { + super.paintComponent(g) + g.drawImage(img.image, 0, 0, width, height, this) + } + + override fun getPreferredSize() = Dimension(img.iconWidth / 2, img.iconHeight / 2) + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/component/OutputPanel.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/component/OutputPanel.kt index 50f999d4..9814c5a1 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/component/OutputPanel.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/component/OutputPanel.kt @@ -1,111 +1,111 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.dialog.component - -import java.awt.Dimension -import java.awt.GridBagConstraints -import java.awt.GridBagLayout -import java.awt.Toolkit -import java.awt.datatransfer.StringSelection -import javax.swing.* - -class OutputPanel( - private val bottomButtonsPanel: BottomButtonsPanel -) : JPanel(GridBagLayout()) { - - val outputArea = JTextArea("") - - constructor(closeCallback: () -> Unit) : this(DefaultBottomButtonsPanel(closeCallback)) - - init { - if(bottomButtonsPanel is DefaultBottomButtonsPanel) { - bottomButtonsPanel.outputTextSupplier = { outputArea.text } - } - - outputArea.isEditable = false - outputArea.highlighter = null - outputArea.lineWrap = true - - val outputScroll = JScrollPane(outputArea) - outputScroll.verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_ALWAYS - outputScroll.horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED - - add(outputScroll, GridBagConstraints().apply { - fill = GridBagConstraints.BOTH - weightx = 0.5 - weighty = 1.0 - }) - - bottomButtonsPanel.create(this) - - add(bottomButtonsPanel, GridBagConstraints().apply { - fill = GridBagConstraints.HORIZONTAL - gridy = 1 - - weightx = 1.0 - ipadx = 10 - ipady = 10 - }) - } - - open class DefaultBottomButtonsPanel( - override val closeCallback: () -> Unit - ) : BottomButtonsPanel() { - val copyButton = JButton("Copy") - val clearButton = JButton("Clear") - val closeButton = JButton("Close") - - var outputTextSupplier: () -> String = { "" } - - override fun create(panel: OutputPanel){ - layout = BoxLayout(this, BoxLayout.LINE_AXIS) - - add(Box.createHorizontalGlue()) - copyButton.addActionListener { - Toolkit.getDefaultToolkit().systemClipboard.setContents(StringSelection(outputTextSupplier()), null) - } - - add(copyButton) - add(Box.createRigidArea(Dimension(4, 0))) - - clearButton.addActionListener { panel.outputArea.text = "" } - - add(clearButton) - add(Box.createRigidArea(Dimension(4, 0))) - - closeButton.addActionListener { closeCallback() } - - add(closeButton) - add(Box.createRigidArea(Dimension(4, 0))) - } - - } - -} - -abstract class BottomButtonsPanel : JPanel() { - abstract val closeCallback: () -> Unit - - abstract fun create(panel: OutputPanel) -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.dialog.component + +import java.awt.Dimension +import java.awt.GridBagConstraints +import java.awt.GridBagLayout +import java.awt.Toolkit +import java.awt.datatransfer.StringSelection +import javax.swing.* + +class OutputPanel( + private val bottomButtonsPanel: BottomButtonsPanel +) : JPanel(GridBagLayout()) { + + val outputArea = JTextArea("") + + constructor(closeCallback: () -> Unit) : this(DefaultBottomButtonsPanel(closeCallback)) + + init { + if(bottomButtonsPanel is DefaultBottomButtonsPanel) { + bottomButtonsPanel.outputTextSupplier = { outputArea.text } + } + + outputArea.isEditable = false + outputArea.highlighter = null + outputArea.lineWrap = true + + val outputScroll = JScrollPane(outputArea) + outputScroll.verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_ALWAYS + outputScroll.horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED + + add(outputScroll, GridBagConstraints().apply { + fill = GridBagConstraints.BOTH + weightx = 0.5 + weighty = 1.0 + }) + + bottomButtonsPanel.create(this) + + add(bottomButtonsPanel, GridBagConstraints().apply { + fill = GridBagConstraints.HORIZONTAL + gridy = 1 + + weightx = 1.0 + ipadx = 10 + ipady = 10 + }) + } + + open class DefaultBottomButtonsPanel( + override val closeCallback: () -> Unit + ) : BottomButtonsPanel() { + val copyButton = JButton("Copy") + val clearButton = JButton("Clear") + val closeButton = JButton("Close") + + var outputTextSupplier: () -> String = { "" } + + override fun create(panel: OutputPanel){ + layout = BoxLayout(this, BoxLayout.LINE_AXIS) + + add(Box.createHorizontalGlue()) + copyButton.addActionListener { + Toolkit.getDefaultToolkit().systemClipboard.setContents(StringSelection(outputTextSupplier()), null) + } + + add(copyButton) + add(Box.createRigidArea(Dimension(4, 0))) + + clearButton.addActionListener { panel.outputArea.text = "" } + + add(clearButton) + add(Box.createRigidArea(Dimension(4, 0))) + + closeButton.addActionListener { closeCallback() } + + add(closeButton) + add(Box.createRigidArea(Dimension(4, 0))) + } + + } + +} + +abstract class BottomButtonsPanel : JPanel() { + abstract val closeCallback: () -> Unit + + abstract fun create(panel: OutputPanel) +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateCameraSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateCameraSource.java index ffc958dd..7bf00cfe 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateCameraSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateCameraSource.java @@ -1,283 +1,283 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.dialog.source; - -import com.github.sarxos.webcam.Webcam; -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.gui.component.input.SizeFields; -import com.github.serivesmejia.eocvsim.input.source.CameraSource; -import com.github.serivesmejia.eocvsim.util.CvUtil; -import com.github.serivesmejia.eocvsim.util.Log; -import org.opencv.core.Mat; -import org.opencv.core.Size; -import org.opencv.videoio.VideoCapture; - -import javax.swing.*; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import java.awt.*; - -public class CreateCameraSource { - - public JDialog createCameraSource = null; - - public JComboBox camerasComboBox = null; - public SizeFields sizeFieldsInput = null; - public JTextField nameTextField = null; - - public JButton createButton = null; - - public boolean wasCancelled = false; - - private final EOCVSim eocvSim; - - private State state = State.INITIAL; - - JLabel statusLabel = new JLabel(); - - enum State { INITIAL, CLICKED_TEST, TEST_SUCCESSFUL, TEST_FAILED } - - public CreateCameraSource(JFrame parent, EOCVSim eocvSim) { - createCameraSource = new JDialog(parent); - - this.eocvSim = eocvSim; - eocvSim.visualizer.childDialogs.add(createCameraSource); - - initCreateImageSource(); - } - - public void initCreateImageSource() { - java.util.List webcams = Webcam.getWebcams(); - - createCameraSource.setModal(true); - - createCameraSource.setTitle("Create camera source"); - createCameraSource.setSize(350, 250); - - JPanel contentsPanel = new JPanel(new GridLayout(5, 1)); - - // Camera id part - JPanel idPanel = new JPanel(new FlowLayout()); - - JLabel idLabel = new JLabel("Camera: "); - idLabel.setHorizontalAlignment(JLabel.LEFT); - - camerasComboBox = new JComboBox<>(); - for(Webcam webcam : webcams) { - camerasComboBox.addItem(webcam.getName()); - } - - SwingUtilities.invokeLater(() -> camerasComboBox.setSelectedIndex(0)); - - idPanel.add(idLabel); - idPanel.add(camerasComboBox); - - contentsPanel.add(idPanel); - - //Name part - - JPanel namePanel = new JPanel(new FlowLayout()); - - JLabel nameLabel = new JLabel("Source Name: "); - - nameTextField = new JTextField("CameraSource-" + (eocvSim.inputSourceManager.sources.size() + 1), 15); - - namePanel.add(nameLabel); - namePanel.add(nameTextField); - - contentsPanel.add(namePanel); - - // Size part - sizeFieldsInput = new SizeFields(); - sizeFieldsInput.onChange.doPersistent(this::updateCreateBtt); - - contentsPanel.add(sizeFieldsInput); - - contentsPanel.setBorder(BorderFactory.createEmptyBorder(15, 0, 0, 0)); - - // Status label part - statusLabel.setHorizontalAlignment(JLabel.CENTER); - - contentsPanel.add(statusLabel); - - // Bottom buttons - JPanel buttonsPanel = new JPanel(new FlowLayout()); - createButton = new JButton(); - - buttonsPanel.add(createButton); - - JButton cancelButton = new JButton("Cancel"); - buttonsPanel.add(cancelButton); - - contentsPanel.add(buttonsPanel); - - //Add contents - contentsPanel.setBorder(BorderFactory.createEmptyBorder(15, 0, 0, 0)); - - createCameraSource.getContentPane().add(contentsPanel, BorderLayout.CENTER); - - // Additional stuff & events - createButton.addActionListener(e -> { - if(state == State.TEST_SUCCESSFUL) { - createSource( - nameTextField.getText(), - camerasComboBox.getSelectedIndex(), - sizeFieldsInput.getCurrentSize() - ); - close(); - } else { - state = State.CLICKED_TEST; - updateState(); - - eocvSim.onMainUpdate.doOnce(() -> { - if (testCamera(camerasComboBox.getSelectedIndex())) { - if (wasCancelled) return; - - SwingUtilities.invokeLater(() -> { - state = State.TEST_SUCCESSFUL; - updateState(); - }); - } else { - SwingUtilities.invokeLater(() -> { - state = State.TEST_FAILED; - updateState(); - }); - } - }); - } - }); - - camerasComboBox.addActionListener((e) -> { - String sourceName = (String)camerasComboBox.getSelectedItem(); - if(!eocvSim.inputSourceManager.isNameOnUse(sourceName)) { - nameTextField.setText(sourceName); - } - - state = State.INITIAL; - updateCreateBtt(); - }); - - nameTextField.getDocument().addDocumentListener(new DocumentListener() { - public void changedUpdate(DocumentEvent e) { - changed(); - } - public void removeUpdate(DocumentEvent e) { changed(); } - public void insertUpdate(DocumentEvent e) { - changed(); - } - public void changed() { - updateCreateBtt(); - } - }); - - cancelButton.addActionListener(e -> { - wasCancelled = true; - close(); - }); - - createCameraSource.setResizable(false); - createCameraSource.setLocationRelativeTo(null); - createCameraSource.setVisible(true); - } - - public void close() { - createCameraSource.setVisible(false); - createCameraSource.dispose(); - } - - public boolean testCamera(int camIndex) { - VideoCapture camera = new VideoCapture(); - camera.open(camerasComboBox.getSelectedIndex()); - - boolean wasOpened = camera.isOpened(); - - if(wasOpened) { - Mat m = new Mat(); - try { - camera.read(m); - Size size = CvUtil.scaleToFit(m.size(), EOCVSim.DEFAULT_EOCV_SIZE); - - SwingUtilities.invokeLater(() -> { - sizeFieldsInput.getWidthTextField().setText(String.format("%.0f", size.width)); - sizeFieldsInput.getHeightTextField().setText(String.format("%.0f", size.height)); - }); - } catch (Exception e) { - Log.warn("CreateCameraSource", "Threw exception when trying to open camera", e); - wasOpened = false; - } - - m.release(); - } - - camera.release(); - - return wasOpened; - } - - private void updateState() { - switch(state) { - case INITIAL: - statusLabel.setText("Click \"test\" to test camera."); - createButton.setText("Test"); - break; - - case CLICKED_TEST: - statusLabel.setText("Trying to open camera, please wait..."); - camerasComboBox.setEnabled(false); - createButton.setEnabled(false); - break; - - case TEST_SUCCESSFUL: - camerasComboBox.setEnabled(true); - createButton.setEnabled(true); - statusLabel.setText("Camera was opened successfully."); - createButton.setText("Create"); - break; - - case TEST_FAILED: - camerasComboBox.setEnabled(true); - createButton.setEnabled(true); - statusLabel.setText("Failed to open camera, try another one."); - createButton.setText("Test"); - break; - } - } - - public void createSource(String sourceName, int index, Size size) { - eocvSim.onMainUpdate.doOnce(() -> eocvSim.inputSourceManager.addInputSource( - sourceName, - new CameraSource(index, size), - true - )); - } - - public void updateCreateBtt() { - createButton.setEnabled(!nameTextField.getText().trim().equals("") - && sizeFieldsInput.getValid() - && !eocvSim.inputSourceManager.isNameOnUse(nameTextField.getText())); - - updateState(); - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.dialog.source; + +import com.github.sarxos.webcam.Webcam; +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.gui.component.input.SizeFields; +import com.github.serivesmejia.eocvsim.input.source.CameraSource; +import com.github.serivesmejia.eocvsim.util.CvUtil; +import com.github.serivesmejia.eocvsim.util.Log; +import org.opencv.core.Mat; +import org.opencv.core.Size; +import org.opencv.videoio.VideoCapture; + +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import java.awt.*; + +public class CreateCameraSource { + + public JDialog createCameraSource = null; + + public JComboBox camerasComboBox = null; + public SizeFields sizeFieldsInput = null; + public JTextField nameTextField = null; + + public JButton createButton = null; + + public boolean wasCancelled = false; + + private final EOCVSim eocvSim; + + private State state = State.INITIAL; + + JLabel statusLabel = new JLabel(); + + enum State { INITIAL, CLICKED_TEST, TEST_SUCCESSFUL, TEST_FAILED } + + public CreateCameraSource(JFrame parent, EOCVSim eocvSim) { + createCameraSource = new JDialog(parent); + + this.eocvSim = eocvSim; + eocvSim.visualizer.childDialogs.add(createCameraSource); + + initCreateImageSource(); + } + + public void initCreateImageSource() { + java.util.List webcams = Webcam.getWebcams(); + + createCameraSource.setModal(true); + + createCameraSource.setTitle("Create camera source"); + createCameraSource.setSize(350, 250); + + JPanel contentsPanel = new JPanel(new GridLayout(5, 1)); + + // Camera id part + JPanel idPanel = new JPanel(new FlowLayout()); + + JLabel idLabel = new JLabel("Camera: "); + idLabel.setHorizontalAlignment(JLabel.LEFT); + + camerasComboBox = new JComboBox<>(); + for(Webcam webcam : webcams) { + camerasComboBox.addItem(webcam.getName()); + } + + SwingUtilities.invokeLater(() -> camerasComboBox.setSelectedIndex(0)); + + idPanel.add(idLabel); + idPanel.add(camerasComboBox); + + contentsPanel.add(idPanel); + + //Name part + + JPanel namePanel = new JPanel(new FlowLayout()); + + JLabel nameLabel = new JLabel("Source Name: "); + + nameTextField = new JTextField("CameraSource-" + (eocvSim.inputSourceManager.sources.size() + 1), 15); + + namePanel.add(nameLabel); + namePanel.add(nameTextField); + + contentsPanel.add(namePanel); + + // Size part + sizeFieldsInput = new SizeFields(); + sizeFieldsInput.onChange.doPersistent(this::updateCreateBtt); + + contentsPanel.add(sizeFieldsInput); + + contentsPanel.setBorder(BorderFactory.createEmptyBorder(15, 0, 0, 0)); + + // Status label part + statusLabel.setHorizontalAlignment(JLabel.CENTER); + + contentsPanel.add(statusLabel); + + // Bottom buttons + JPanel buttonsPanel = new JPanel(new FlowLayout()); + createButton = new JButton(); + + buttonsPanel.add(createButton); + + JButton cancelButton = new JButton("Cancel"); + buttonsPanel.add(cancelButton); + + contentsPanel.add(buttonsPanel); + + //Add contents + contentsPanel.setBorder(BorderFactory.createEmptyBorder(15, 0, 0, 0)); + + createCameraSource.getContentPane().add(contentsPanel, BorderLayout.CENTER); + + // Additional stuff & events + createButton.addActionListener(e -> { + if(state == State.TEST_SUCCESSFUL) { + createSource( + nameTextField.getText(), + camerasComboBox.getSelectedIndex(), + sizeFieldsInput.getCurrentSize() + ); + close(); + } else { + state = State.CLICKED_TEST; + updateState(); + + eocvSim.onMainUpdate.doOnce(() -> { + if (testCamera(camerasComboBox.getSelectedIndex())) { + if (wasCancelled) return; + + SwingUtilities.invokeLater(() -> { + state = State.TEST_SUCCESSFUL; + updateState(); + }); + } else { + SwingUtilities.invokeLater(() -> { + state = State.TEST_FAILED; + updateState(); + }); + } + }); + } + }); + + camerasComboBox.addActionListener((e) -> { + String sourceName = (String)camerasComboBox.getSelectedItem(); + if(!eocvSim.inputSourceManager.isNameOnUse(sourceName)) { + nameTextField.setText(sourceName); + } + + state = State.INITIAL; + updateCreateBtt(); + }); + + nameTextField.getDocument().addDocumentListener(new DocumentListener() { + public void changedUpdate(DocumentEvent e) { + changed(); + } + public void removeUpdate(DocumentEvent e) { changed(); } + public void insertUpdate(DocumentEvent e) { + changed(); + } + public void changed() { + updateCreateBtt(); + } + }); + + cancelButton.addActionListener(e -> { + wasCancelled = true; + close(); + }); + + createCameraSource.setResizable(false); + createCameraSource.setLocationRelativeTo(null); + createCameraSource.setVisible(true); + } + + public void close() { + createCameraSource.setVisible(false); + createCameraSource.dispose(); + } + + public boolean testCamera(int camIndex) { + VideoCapture camera = new VideoCapture(); + camera.open(camerasComboBox.getSelectedIndex()); + + boolean wasOpened = camera.isOpened(); + + if(wasOpened) { + Mat m = new Mat(); + try { + camera.read(m); + Size size = CvUtil.scaleToFit(m.size(), EOCVSim.DEFAULT_EOCV_SIZE); + + SwingUtilities.invokeLater(() -> { + sizeFieldsInput.getWidthTextField().setText(String.format("%.0f", size.width)); + sizeFieldsInput.getHeightTextField().setText(String.format("%.0f", size.height)); + }); + } catch (Exception e) { + Log.warn("CreateCameraSource", "Threw exception when trying to open camera", e); + wasOpened = false; + } + + m.release(); + } + + camera.release(); + + return wasOpened; + } + + private void updateState() { + switch(state) { + case INITIAL: + statusLabel.setText("Click \"test\" to test camera."); + createButton.setText("Test"); + break; + + case CLICKED_TEST: + statusLabel.setText("Trying to open camera, please wait..."); + camerasComboBox.setEnabled(false); + createButton.setEnabled(false); + break; + + case TEST_SUCCESSFUL: + camerasComboBox.setEnabled(true); + createButton.setEnabled(true); + statusLabel.setText("Camera was opened successfully."); + createButton.setText("Create"); + break; + + case TEST_FAILED: + camerasComboBox.setEnabled(true); + createButton.setEnabled(true); + statusLabel.setText("Failed to open camera, try another one."); + createButton.setText("Test"); + break; + } + } + + public void createSource(String sourceName, int index, Size size) { + eocvSim.onMainUpdate.doOnce(() -> eocvSim.inputSourceManager.addInputSource( + sourceName, + new CameraSource(index, size), + true + )); + } + + public void updateCreateBtt() { + createButton.setEnabled(!nameTextField.getText().trim().equals("") + && sizeFieldsInput.getValid() + && !eocvSim.inputSourceManager.isNameOnUse(nameTextField.getText())); + + updateState(); + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateImageSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateImageSource.java index a7a9e9b5..0d157698 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateImageSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateImageSource.java @@ -1,211 +1,211 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.dialog.source; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.gui.component.input.FileSelector; -import com.github.serivesmejia.eocvsim.gui.component.input.SizeFields; -import com.github.serivesmejia.eocvsim.input.source.ImageSource; -import com.github.serivesmejia.eocvsim.util.CvUtil; -import com.github.serivesmejia.eocvsim.util.FileFilters; -import com.github.serivesmejia.eocvsim.util.StrUtil; -import org.opencv.core.Size; - -import javax.swing.*; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import java.awt.*; -import java.io.File; - -public class CreateImageSource { - - public JDialog createImageSource; - - public JTextField nameTextField = null; - - public SizeFields sizeFieldsInput = null; - - public FileSelector imageFileSelector = null; - - private File initialFile = null; - - public JButton createButton = null; - public boolean selectedValidImage = false; - private EOCVSim eocvSim = null; - - public CreateImageSource(JFrame parent, EOCVSim eocvSim, File initialFile) { - createImageSource = new JDialog(parent); - - this.eocvSim = eocvSim; - this.initialFile = initialFile; - - eocvSim.visualizer.childDialogs.add(createImageSource); - - initCreateImageSource(); - } - - public void initCreateImageSource() { - - createImageSource.setModal(true); - - createImageSource.setTitle("Create image source"); - createImageSource.setSize(370, 200); - - JPanel contentsPanel = new JPanel(new GridLayout(4, 1)); - - //file select part - - imageFileSelector = new FileSelector(18, FileFilters.imagesFilter); - - imageFileSelector.onFileSelect.doPersistent(() -> - imageFileSelected(imageFileSelector.getLastSelectedFile()) - ); - - - if(initialFile != null) - SwingUtilities.invokeLater(() -> - imageFileSelector.setLastSelectedFile(initialFile) - ); - - contentsPanel.add(imageFileSelector); - - // Size part - - sizeFieldsInput = new SizeFields(); - sizeFieldsInput.onChange.doPersistent(this::updateCreateBtt); - - contentsPanel.add(sizeFieldsInput); - contentsPanel.setBorder(BorderFactory.createEmptyBorder(15, 0, 0, 0)); - - //Name part - - JPanel namePanel = new JPanel(new FlowLayout()); - - JLabel nameLabel = new JLabel("Source name: "); - nameLabel.setHorizontalAlignment(JLabel.LEFT); - - nameTextField = new JTextField("ImageSource-" + (eocvSim.inputSourceManager.sources.size() + 1), 15); - - namePanel.add(nameLabel); - namePanel.add(nameTextField); - - contentsPanel.add(namePanel); - - // Bottom buttons - - JPanel buttonsPanel = new JPanel(new FlowLayout()); - createButton = new JButton("Create"); - createButton.setEnabled(selectedValidImage); - - buttonsPanel.add(createButton); - - JButton cancelButton = new JButton("Cancel"); - buttonsPanel.add(cancelButton); - - contentsPanel.add(buttonsPanel); - - //Add contents - - createImageSource.getContentPane().add(contentsPanel, BorderLayout.CENTER); - - // Additional stuff & events - - nameTextField.getDocument().addDocumentListener(new DocumentListener() { - public void changedUpdate(DocumentEvent e) { - changed(); - } - - public void removeUpdate(DocumentEvent e) { - changed(); - } - - public void insertUpdate(DocumentEvent e) { - changed(); - } - - public void changed() { - updateCreateBtt(); - } - }); - - createButton.addActionListener(e -> { - createSource(nameTextField.getText(), imageFileSelector.getLastSelectedFile().getAbsolutePath(), sizeFieldsInput.getCurrentSize()); - close(); - }); - - cancelButton.addActionListener(e -> close()); - - createImageSource.setResizable(false); - createImageSource.setLocationRelativeTo(null); - createImageSource.setVisible(true); - - } - - public void imageFileSelected(File f) { - String fileAbsPath = f.getAbsolutePath(); - - if (CvUtil.checkImageValid(fileAbsPath)) { - - String fileName = StrUtil.getFileBaseName(f.getName()); - if(!fileName.trim().equals("") && !eocvSim.inputSourceManager.isNameOnUse(fileName)) { - nameTextField.setText(fileName); - } - - Size size = CvUtil.scaleToFit(CvUtil.getImageSize(fileAbsPath), EOCVSim.DEFAULT_EOCV_SIZE); - - sizeFieldsInput.getWidthTextField().setText(String.valueOf(Math.round(size.width))); - sizeFieldsInput.getHeightTextField().setText(String.valueOf(Math.round(size.height))); - - selectedValidImage = true; - } else { - imageFileSelector.getDirTextField().setText("Unable to load selected file."); - selectedValidImage = false; - } - - updateCreateBtt(); - } - - public void close() { - createImageSource.setVisible(false); - createImageSource.dispose(); - } - - public void createSource(String sourceName, String imgPath, Size size) { - eocvSim.onMainUpdate.doOnce(() -> - eocvSim.inputSourceManager.addInputSource( - sourceName, - new ImageSource(imgPath, size), - false - ) - ); - } - - public void updateCreateBtt() { - createButton.setEnabled(!nameTextField.getText().trim().equals("") - && sizeFieldsInput.getValid() - && selectedValidImage - && !eocvSim.inputSourceManager.isNameOnUse(nameTextField.getText())); - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.dialog.source; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.gui.component.input.FileSelector; +import com.github.serivesmejia.eocvsim.gui.component.input.SizeFields; +import com.github.serivesmejia.eocvsim.input.source.ImageSource; +import com.github.serivesmejia.eocvsim.util.CvUtil; +import com.github.serivesmejia.eocvsim.util.FileFilters; +import com.github.serivesmejia.eocvsim.util.StrUtil; +import org.opencv.core.Size; + +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import java.awt.*; +import java.io.File; + +public class CreateImageSource { + + public JDialog createImageSource; + + public JTextField nameTextField = null; + + public SizeFields sizeFieldsInput = null; + + public FileSelector imageFileSelector = null; + + private File initialFile = null; + + public JButton createButton = null; + public boolean selectedValidImage = false; + private EOCVSim eocvSim = null; + + public CreateImageSource(JFrame parent, EOCVSim eocvSim, File initialFile) { + createImageSource = new JDialog(parent); + + this.eocvSim = eocvSim; + this.initialFile = initialFile; + + eocvSim.visualizer.childDialogs.add(createImageSource); + + initCreateImageSource(); + } + + public void initCreateImageSource() { + + createImageSource.setModal(true); + + createImageSource.setTitle("Create image source"); + createImageSource.setSize(370, 200); + + JPanel contentsPanel = new JPanel(new GridLayout(4, 1)); + + //file select part + + imageFileSelector = new FileSelector(18, FileFilters.imagesFilter); + + imageFileSelector.onFileSelect.doPersistent(() -> + imageFileSelected(imageFileSelector.getLastSelectedFile()) + ); + + + if(initialFile != null) + SwingUtilities.invokeLater(() -> + imageFileSelector.setLastSelectedFile(initialFile) + ); + + contentsPanel.add(imageFileSelector); + + // Size part + + sizeFieldsInput = new SizeFields(); + sizeFieldsInput.onChange.doPersistent(this::updateCreateBtt); + + contentsPanel.add(sizeFieldsInput); + contentsPanel.setBorder(BorderFactory.createEmptyBorder(15, 0, 0, 0)); + + //Name part + + JPanel namePanel = new JPanel(new FlowLayout()); + + JLabel nameLabel = new JLabel("Source name: "); + nameLabel.setHorizontalAlignment(JLabel.LEFT); + + nameTextField = new JTextField("ImageSource-" + (eocvSim.inputSourceManager.sources.size() + 1), 15); + + namePanel.add(nameLabel); + namePanel.add(nameTextField); + + contentsPanel.add(namePanel); + + // Bottom buttons + + JPanel buttonsPanel = new JPanel(new FlowLayout()); + createButton = new JButton("Create"); + createButton.setEnabled(selectedValidImage); + + buttonsPanel.add(createButton); + + JButton cancelButton = new JButton("Cancel"); + buttonsPanel.add(cancelButton); + + contentsPanel.add(buttonsPanel); + + //Add contents + + createImageSource.getContentPane().add(contentsPanel, BorderLayout.CENTER); + + // Additional stuff & events + + nameTextField.getDocument().addDocumentListener(new DocumentListener() { + public void changedUpdate(DocumentEvent e) { + changed(); + } + + public void removeUpdate(DocumentEvent e) { + changed(); + } + + public void insertUpdate(DocumentEvent e) { + changed(); + } + + public void changed() { + updateCreateBtt(); + } + }); + + createButton.addActionListener(e -> { + createSource(nameTextField.getText(), imageFileSelector.getLastSelectedFile().getAbsolutePath(), sizeFieldsInput.getCurrentSize()); + close(); + }); + + cancelButton.addActionListener(e -> close()); + + createImageSource.setResizable(false); + createImageSource.setLocationRelativeTo(null); + createImageSource.setVisible(true); + + } + + public void imageFileSelected(File f) { + String fileAbsPath = f.getAbsolutePath(); + + if (CvUtil.checkImageValid(fileAbsPath)) { + + String fileName = StrUtil.getFileBaseName(f.getName()); + if(!fileName.trim().equals("") && !eocvSim.inputSourceManager.isNameOnUse(fileName)) { + nameTextField.setText(fileName); + } + + Size size = CvUtil.scaleToFit(CvUtil.getImageSize(fileAbsPath), EOCVSim.DEFAULT_EOCV_SIZE); + + sizeFieldsInput.getWidthTextField().setText(String.valueOf(Math.round(size.width))); + sizeFieldsInput.getHeightTextField().setText(String.valueOf(Math.round(size.height))); + + selectedValidImage = true; + } else { + imageFileSelector.getDirTextField().setText("Unable to load selected file."); + selectedValidImage = false; + } + + updateCreateBtt(); + } + + public void close() { + createImageSource.setVisible(false); + createImageSource.dispose(); + } + + public void createSource(String sourceName, String imgPath, Size size) { + eocvSim.onMainUpdate.doOnce(() -> + eocvSim.inputSourceManager.addInputSource( + sourceName, + new ImageSource(imgPath, size), + false + ) + ); + } + + public void updateCreateBtt() { + createButton.setEnabled(!nameTextField.getText().trim().equals("") + && sizeFieldsInput.getValid() + && selectedValidImage + && !eocvSim.inputSourceManager.isNameOnUse(nameTextField.getText())); + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateSource.java index 7cedd0ab..abc88ad5 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateSource.java @@ -1,112 +1,112 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.dialog.source; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.gui.DialogFactory; -import com.github.serivesmejia.eocvsim.input.SourceType; - -import javax.swing.*; -import java.awt.*; - -public class CreateSource { - - public static volatile boolean alreadyOpened = false; - public volatile JDialog chooseSource = null; - - EOCVSim eocvSim = null; - - private volatile JFrame parent = null; - - public CreateSource(JFrame parent, EOCVSim eocvSim) { - - chooseSource = new JDialog(parent); - - this.parent = parent; - this.eocvSim = eocvSim; - - eocvSim.visualizer.childDialogs.add(chooseSource); - initChooseSource(); - - } - - private void initChooseSource() { - - alreadyOpened = true; - - chooseSource.setModal(true); - - chooseSource.setTitle("Select source type"); - chooseSource.setSize(300, 150); - - JPanel contentsPane = new JPanel(new GridLayout(2, 1)); - - JPanel dropDownPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); - - SourceType[] sourceTypes = SourceType.values(); - String[] sourceTypesStr = new String[sourceTypes.length - 1]; - - for (int i = 0; i < sourceTypes.length - 1; i++) { - sourceTypesStr[i] = sourceTypes[i].coolName; - } - - JComboBox dropDown = new JComboBox<>(sourceTypesStr); - dropDownPanel.add(dropDown); - contentsPane.add(dropDownPanel); - - JPanel buttonsPanel = new JPanel(new FlowLayout()); - JButton nextButton = new JButton("Next"); - - buttonsPanel.add(nextButton); - - JButton cancelButton = new JButton("Cancel"); - buttonsPanel.add(cancelButton); - - contentsPane.add(buttonsPanel); - - contentsPane.setBorder(BorderFactory.createEmptyBorder(15, 0, 0, 0)); - - chooseSource.getContentPane().add(contentsPane, BorderLayout.CENTER); - - cancelButton.addActionListener(e -> close()); - - nextButton.addActionListener(e -> { - close(); - SourceType sourceType = SourceType.fromCoolName(dropDown.getSelectedItem().toString()); - DialogFactory.createSourceDialog(eocvSim, sourceType); - }); - - chooseSource.setResizable(false); - chooseSource.setLocationRelativeTo(null); - chooseSource.setVisible(true); - - } - - public void close() { - alreadyOpened = false; - chooseSource.setVisible(false); - chooseSource.dispose(); - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.dialog.source; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.gui.DialogFactory; +import com.github.serivesmejia.eocvsim.input.SourceType; + +import javax.swing.*; +import java.awt.*; + +public class CreateSource { + + public static volatile boolean alreadyOpened = false; + public volatile JDialog chooseSource = null; + + EOCVSim eocvSim = null; + + private volatile JFrame parent = null; + + public CreateSource(JFrame parent, EOCVSim eocvSim) { + + chooseSource = new JDialog(parent); + + this.parent = parent; + this.eocvSim = eocvSim; + + eocvSim.visualizer.childDialogs.add(chooseSource); + initChooseSource(); + + } + + private void initChooseSource() { + + alreadyOpened = true; + + chooseSource.setModal(true); + + chooseSource.setTitle("Select source type"); + chooseSource.setSize(300, 150); + + JPanel contentsPane = new JPanel(new GridLayout(2, 1)); + + JPanel dropDownPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); + + SourceType[] sourceTypes = SourceType.values(); + String[] sourceTypesStr = new String[sourceTypes.length - 1]; + + for (int i = 0; i < sourceTypes.length - 1; i++) { + sourceTypesStr[i] = sourceTypes[i].coolName; + } + + JComboBox dropDown = new JComboBox<>(sourceTypesStr); + dropDownPanel.add(dropDown); + contentsPane.add(dropDownPanel); + + JPanel buttonsPanel = new JPanel(new FlowLayout()); + JButton nextButton = new JButton("Next"); + + buttonsPanel.add(nextButton); + + JButton cancelButton = new JButton("Cancel"); + buttonsPanel.add(cancelButton); + + contentsPane.add(buttonsPanel); + + contentsPane.setBorder(BorderFactory.createEmptyBorder(15, 0, 0, 0)); + + chooseSource.getContentPane().add(contentsPane, BorderLayout.CENTER); + + cancelButton.addActionListener(e -> close()); + + nextButton.addActionListener(e -> { + close(); + SourceType sourceType = SourceType.fromCoolName(dropDown.getSelectedItem().toString()); + DialogFactory.createSourceDialog(eocvSim, sourceType); + }); + + chooseSource.setResizable(false); + chooseSource.setLocationRelativeTo(null); + chooseSource.setVisible(true); + + } + + public void close() { + alreadyOpened = false; + chooseSource.setVisible(false); + chooseSource.dispose(); + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateVideoSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateVideoSource.java index 5b3a0ac6..6290705f 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateVideoSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateVideoSource.java @@ -1,221 +1,221 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.dialog.source; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.gui.DialogFactory; -import com.github.serivesmejia.eocvsim.gui.component.input.FileSelector; -import com.github.serivesmejia.eocvsim.gui.component.input.SizeFields; -import com.github.serivesmejia.eocvsim.gui.util.GuiUtil; -import com.github.serivesmejia.eocvsim.input.source.VideoSource; -import com.github.serivesmejia.eocvsim.util.CvUtil; -import com.github.serivesmejia.eocvsim.util.FileFilters; -import com.github.serivesmejia.eocvsim.util.Log; -import com.github.serivesmejia.eocvsim.util.StrUtil; -import org.opencv.core.Mat; -import org.opencv.core.Size; - -import javax.swing.*; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import javax.swing.filechooser.FileNameExtensionFilter; -import java.awt.*; -import java.io.File; - -public class CreateVideoSource { - - public JDialog createVideoSource = null; - - public JTextField nameTextField = null; - - public FileSelector fileSelectorVideo = null; - - public SizeFields sizeFieldsInput = null; - - public JButton createButton = null; - public boolean selectedValidVideo = false; - - public File initialFile = null; - - private EOCVSim eocvSim = null; - - public CreateVideoSource(JFrame parent, EOCVSim eocvSim, File initialFile) { - - createVideoSource = new JDialog(parent); - - this.eocvSim = eocvSim; - this.initialFile = initialFile; - - eocvSim.visualizer.childDialogs.add(createVideoSource); - - initCreateImageSource(); - - } - - public void initCreateImageSource() { - - createVideoSource.setModal(true); - - createVideoSource.setTitle("Create video source"); - createVideoSource.setSize(370, 200); - - JPanel contentsPanel = new JPanel(new GridLayout(4, 1)); - - //file select - fileSelectorVideo = new FileSelector(18, FileFilters.videoMediaFilter); - - fileSelectorVideo.onFileSelect.doPersistent(() -> - videoFileSelected(fileSelectorVideo.getLastSelectedFile()) - ); - - if(initialFile != null) - SwingUtilities.invokeLater(() -> - fileSelectorVideo.setLastSelectedFile(initialFile) - ); - - contentsPanel.add(fileSelectorVideo); - - // Size part - - sizeFieldsInput = new SizeFields(); - sizeFieldsInput.onChange.doPersistent(this::updateCreateBtt); - - contentsPanel.add(sizeFieldsInput); - - contentsPanel.setBorder(BorderFactory.createEmptyBorder(15, 0, 0, 0)); - - //Name part - - JPanel namePanel = new JPanel(new FlowLayout()); - - JLabel nameLabel = new JLabel("Source name: "); - nameLabel.setHorizontalAlignment(JLabel.LEFT); - - nameTextField = new JTextField("VideoSource-" + (eocvSim.inputSourceManager.sources.size() + 1), 15); - - namePanel.add(nameLabel); - namePanel.add(nameTextField); - - contentsPanel.add(namePanel); - - // Bottom buttons - JPanel buttonsPanel = new JPanel(new FlowLayout()); - createButton = new JButton("Create"); - createButton.setEnabled(selectedValidVideo); - - buttonsPanel.add(createButton); - - JButton cancelButton = new JButton("Cancel"); - buttonsPanel.add(cancelButton); - - contentsPanel.add(buttonsPanel); - - //Add contents - createVideoSource.getContentPane().add(contentsPanel, BorderLayout.CENTER); - - nameTextField.getDocument().addDocumentListener(new DocumentListener() { - public void changedUpdate(DocumentEvent e) { - changed(); - } - - public void removeUpdate(DocumentEvent e) { - changed(); - } - - public void insertUpdate(DocumentEvent e) { - changed(); - } - - public void changed() { - updateCreateBtt(); - } - }); - - createButton.addActionListener(e -> { - createSource(nameTextField.getText(), fileSelectorVideo.getLastSelectedFile().getAbsolutePath(), sizeFieldsInput.getCurrentSize()); - close(); - }); - - // Status label part - cancelButton.addActionListener(e -> close()); - - createVideoSource.setResizable(false); - createVideoSource.setLocationRelativeTo(null); - createVideoSource.setVisible(true); - - } - - public void videoFileSelected(File f) { - - String fileAbsPath = f.getAbsolutePath(); - - Mat videoMat = CvUtil.readOnceFromVideo(fileAbsPath); - - if (videoMat != null && !videoMat.empty()) { - - String fileName = StrUtil.getFileBaseName(f.getName()); - if(!fileName.trim().equals("") && !eocvSim.inputSourceManager.isNameOnUse(fileName)) { - nameTextField.setText(fileName); - } - - Size newSize = CvUtil.scaleToFit(videoMat.size(), EOCVSim.DEFAULT_EOCV_SIZE); - - this.sizeFieldsInput.getWidthTextField().setText(String.valueOf(Math.round(newSize.width))); - this.sizeFieldsInput.getHeightTextField().setText(String.valueOf(Math.round(newSize.height))); - - selectedValidVideo = true; - } else { - fileSelectorVideo.getDirTextField().setText("Unable to load selected file."); - selectedValidVideo = false; - } - - if(videoMat != null) videoMat.release(); - - updateCreateBtt(); - - } - - public void close() { - createVideoSource.setVisible(false); - createVideoSource.dispose(); - } - - public void createSource(String sourceName, String videoPath, Size size) { - eocvSim.onMainUpdate.doOnce(() -> { - eocvSim.inputSourceManager.addInputSource( - sourceName, - new VideoSource(videoPath, size), - true - ); - }); - } - - public void updateCreateBtt() { - createButton.setEnabled(!nameTextField.getText().trim().equals("") - && sizeFieldsInput.getValid() - && selectedValidVideo - && !eocvSim.inputSourceManager.isNameOnUse(nameTextField.getText())); - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.dialog.source; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.gui.DialogFactory; +import com.github.serivesmejia.eocvsim.gui.component.input.FileSelector; +import com.github.serivesmejia.eocvsim.gui.component.input.SizeFields; +import com.github.serivesmejia.eocvsim.gui.util.GuiUtil; +import com.github.serivesmejia.eocvsim.input.source.VideoSource; +import com.github.serivesmejia.eocvsim.util.CvUtil; +import com.github.serivesmejia.eocvsim.util.FileFilters; +import com.github.serivesmejia.eocvsim.util.Log; +import com.github.serivesmejia.eocvsim.util.StrUtil; +import org.opencv.core.Mat; +import org.opencv.core.Size; + +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.filechooser.FileNameExtensionFilter; +import java.awt.*; +import java.io.File; + +public class CreateVideoSource { + + public JDialog createVideoSource = null; + + public JTextField nameTextField = null; + + public FileSelector fileSelectorVideo = null; + + public SizeFields sizeFieldsInput = null; + + public JButton createButton = null; + public boolean selectedValidVideo = false; + + public File initialFile = null; + + private EOCVSim eocvSim = null; + + public CreateVideoSource(JFrame parent, EOCVSim eocvSim, File initialFile) { + + createVideoSource = new JDialog(parent); + + this.eocvSim = eocvSim; + this.initialFile = initialFile; + + eocvSim.visualizer.childDialogs.add(createVideoSource); + + initCreateImageSource(); + + } + + public void initCreateImageSource() { + + createVideoSource.setModal(true); + + createVideoSource.setTitle("Create video source"); + createVideoSource.setSize(370, 200); + + JPanel contentsPanel = new JPanel(new GridLayout(4, 1)); + + //file select + fileSelectorVideo = new FileSelector(18, FileFilters.videoMediaFilter); + + fileSelectorVideo.onFileSelect.doPersistent(() -> + videoFileSelected(fileSelectorVideo.getLastSelectedFile()) + ); + + if(initialFile != null) + SwingUtilities.invokeLater(() -> + fileSelectorVideo.setLastSelectedFile(initialFile) + ); + + contentsPanel.add(fileSelectorVideo); + + // Size part + + sizeFieldsInput = new SizeFields(); + sizeFieldsInput.onChange.doPersistent(this::updateCreateBtt); + + contentsPanel.add(sizeFieldsInput); + + contentsPanel.setBorder(BorderFactory.createEmptyBorder(15, 0, 0, 0)); + + //Name part + + JPanel namePanel = new JPanel(new FlowLayout()); + + JLabel nameLabel = new JLabel("Source name: "); + nameLabel.setHorizontalAlignment(JLabel.LEFT); + + nameTextField = new JTextField("VideoSource-" + (eocvSim.inputSourceManager.sources.size() + 1), 15); + + namePanel.add(nameLabel); + namePanel.add(nameTextField); + + contentsPanel.add(namePanel); + + // Bottom buttons + JPanel buttonsPanel = new JPanel(new FlowLayout()); + createButton = new JButton("Create"); + createButton.setEnabled(selectedValidVideo); + + buttonsPanel.add(createButton); + + JButton cancelButton = new JButton("Cancel"); + buttonsPanel.add(cancelButton); + + contentsPanel.add(buttonsPanel); + + //Add contents + createVideoSource.getContentPane().add(contentsPanel, BorderLayout.CENTER); + + nameTextField.getDocument().addDocumentListener(new DocumentListener() { + public void changedUpdate(DocumentEvent e) { + changed(); + } + + public void removeUpdate(DocumentEvent e) { + changed(); + } + + public void insertUpdate(DocumentEvent e) { + changed(); + } + + public void changed() { + updateCreateBtt(); + } + }); + + createButton.addActionListener(e -> { + createSource(nameTextField.getText(), fileSelectorVideo.getLastSelectedFile().getAbsolutePath(), sizeFieldsInput.getCurrentSize()); + close(); + }); + + // Status label part + cancelButton.addActionListener(e -> close()); + + createVideoSource.setResizable(false); + createVideoSource.setLocationRelativeTo(null); + createVideoSource.setVisible(true); + + } + + public void videoFileSelected(File f) { + + String fileAbsPath = f.getAbsolutePath(); + + Mat videoMat = CvUtil.readOnceFromVideo(fileAbsPath); + + if (videoMat != null && !videoMat.empty()) { + + String fileName = StrUtil.getFileBaseName(f.getName()); + if(!fileName.trim().equals("") && !eocvSim.inputSourceManager.isNameOnUse(fileName)) { + nameTextField.setText(fileName); + } + + Size newSize = CvUtil.scaleToFit(videoMat.size(), EOCVSim.DEFAULT_EOCV_SIZE); + + this.sizeFieldsInput.getWidthTextField().setText(String.valueOf(Math.round(newSize.width))); + this.sizeFieldsInput.getHeightTextField().setText(String.valueOf(Math.round(newSize.height))); + + selectedValidVideo = true; + } else { + fileSelectorVideo.getDirTextField().setText("Unable to load selected file."); + selectedValidVideo = false; + } + + if(videoMat != null) videoMat.release(); + + updateCreateBtt(); + + } + + public void close() { + createVideoSource.setVisible(false); + createVideoSource.dispose(); + } + + public void createSource(String sourceName, String videoPath, Size size) { + eocvSim.onMainUpdate.doOnce(() -> { + eocvSim.inputSourceManager.addInputSource( + sourceName, + new VideoSource(videoPath, size), + true + ); + }); + } + + public void updateCreateBtt() { + createButton.setEnabled(!nameTextField.getText().trim().equals("") + && sizeFieldsInput.getValid() + && selectedValidVideo + && !eocvSim.inputSourceManager.isNameOnUse(nameTextField.getText())); + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/theme/Theme.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/theme/Theme.java index 390aa801..98fb99c0 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/theme/Theme.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/theme/Theme.java @@ -1,57 +1,57 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.theme; - -import com.formdev.flatlaf.*; -import com.formdev.flatlaf.intellijthemes.*; - -import javax.swing.*; - -public enum Theme { - - Default(() -> { - UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); - }), - Light(FlatLightLaf::setup), - Dark(FlatDarkLaf::setup), - Darcula(FlatDarculaLaf::setup), - Light_Intellij(FlatIntelliJLaf::setup), - Light_Flat_Intellij(FlatLightFlatIJTheme::setup), - Cyan_Light_Intellij(FlatCyanLightIJTheme::setup), - High_Contrast_Intellij(FlatHighContrastIJTheme::setup), - Dracula_Intellij(FlatDraculaIJTheme::setup), - Dark_Flat_Intellij(FlatDarkFlatIJTheme::setup), - Spacegray_Intellij(FlatSpacegrayIJTheme::setup), - Material_Dark_Intellij(FlatMaterialDesignDarkIJTheme::setup); - - ThemeInstaller installRunn; - - Theme(ThemeInstaller installRunn) { - this.installRunn = installRunn; - } - - public void install() throws ClassNotFoundException, UnsupportedLookAndFeelException, InstantiationException, IllegalAccessException { - installRunn.install(); - } +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.theme; + +import com.formdev.flatlaf.*; +import com.formdev.flatlaf.intellijthemes.*; + +import javax.swing.*; + +public enum Theme { + + Default(() -> { + UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); + }), + Light(FlatLightLaf::setup), + Dark(FlatDarkLaf::setup), + Darcula(FlatDarculaLaf::setup), + Light_Intellij(FlatIntelliJLaf::setup), + Light_Flat_Intellij(FlatLightFlatIJTheme::setup), + Cyan_Light_Intellij(FlatCyanLightIJTheme::setup), + High_Contrast_Intellij(FlatHighContrastIJTheme::setup), + Dracula_Intellij(FlatDraculaIJTheme::setup), + Dark_Flat_Intellij(FlatDarkFlatIJTheme::setup), + Spacegray_Intellij(FlatSpacegrayIJTheme::setup), + Material_Dark_Intellij(FlatMaterialDesignDarkIJTheme::setup); + + ThemeInstaller installRunn; + + Theme(ThemeInstaller installRunn) { + this.installRunn = installRunn; + } + + public void install() throws ClassNotFoundException, UnsupportedLookAndFeelException, InstantiationException, IllegalAccessException { + installRunn.install(); + } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/theme/ThemeInstaller.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/theme/ThemeInstaller.java index 9635ec5e..43d079f1 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/theme/ThemeInstaller.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/theme/ThemeInstaller.java @@ -1,7 +1,7 @@ -package com.github.serivesmejia.eocvsim.gui.theme; - -import javax.swing.*; - -public interface ThemeInstaller { - void install() throws ClassNotFoundException, UnsupportedLookAndFeelException, InstantiationException, IllegalAccessException; -} +package com.github.serivesmejia.eocvsim.gui.theme; + +import javax.swing.*; + +public interface ThemeInstaller { + void install() throws ClassNotFoundException, UnsupportedLookAndFeelException, InstantiationException, IllegalAccessException; +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/GuiUtil.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/GuiUtil.java index c419bea7..a2beb6f5 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/GuiUtil.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/GuiUtil.java @@ -1,245 +1,245 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.util; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.gui.DialogFactory; -import com.github.serivesmejia.eocvsim.gui.dialog.FileAlreadyExists; -import com.github.serivesmejia.eocvsim.util.CvUtil; -import com.github.serivesmejia.eocvsim.util.Log; -import com.github.serivesmejia.eocvsim.util.SysUtil; -import org.opencv.core.Mat; - -import javax.imageio.ImageIO; -import javax.swing.*; -import javax.swing.filechooser.FileNameExtensionFilter; -import javax.swing.text.AbstractDocument; -import javax.swing.text.AttributeSet; -import javax.swing.text.BadLocationException; -import javax.swing.text.DocumentFilter; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public final class GuiUtil { - - public static void jTextFieldOnlyNumbers(JTextField field, int minNumber, int onMinNumberChangeTo) { - - ((AbstractDocument) field.getDocument()).setDocumentFilter(new DocumentFilter() { - final Pattern regEx = Pattern.compile("\\d*"); - - @Override - public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException { - Matcher matcher = regEx.matcher(text); - if (!matcher.matches()) { - return; - } - - if (field.getText().length() == 0) { - try { - int number = Integer.parseInt(text); - if (number <= minNumber) { - text = String.valueOf(onMinNumberChangeTo); - } - } catch (NumberFormatException ex) { - } - } - - super.replace(fb, offset, length, text, attrs); - } - }); - - } - - public static ImageIcon scaleImage(ImageIcon icon, int w, int h) { - - int nw = icon.getIconWidth(); - int nh = icon.getIconHeight(); - - if (icon.getIconWidth() > w) { - nw = w; - nh = (nw * icon.getIconHeight()) / icon.getIconWidth(); - } - - if (nh > h) { - nh = h; - nw = (icon.getIconWidth() * nh) / icon.getIconHeight(); - } - - return new ImageIcon(icon.getImage().getScaledInstance(nw, nh, Image.SCALE_SMOOTH)); - - } - - public static ImageIcon loadImageIcon(String path) throws IOException { - return new ImageIcon(loadBufferedImage(path)); - } - - public static BufferedImage loadBufferedImage(String path) throws IOException { - return ImageIO.read(GuiUtil.class.getResourceAsStream(path)); - } - - public static void saveBufferedImage(File file, BufferedImage bufferedImage, String format) throws IOException { - ImageIO.write(bufferedImage, format, file); - } - - public static void saveBufferedImage(File file, BufferedImage bufferedImage) throws IOException { - saveBufferedImage(file, bufferedImage, "jpg"); - } - - public static void catchSaveBufferedImage(File file, BufferedImage bufferedImage, String format) { - try { - saveBufferedImage(file, bufferedImage, format); - } catch (IOException e) { - Log.error("GuiUtil", "Failed to save buffered image", e); - } - } - - public static void catchSaveBufferedImage(File file, BufferedImage bufferedImage) { - catchSaveBufferedImage(file, bufferedImage, "jpg"); - } - - public static void invertBufferedImageColors(BufferedImage input) { - - for (int x = 0; x < input.getWidth(); x++) { - for (int y = 0; y < input.getHeight(); y++) { - - int rgba = input.getRGB(x, y); - Color col = new Color(rgba, true); - - if (col.getAlpha() <= 0) continue; - - col = new Color(255 - col.getRed(), - 255 - col.getGreen(), - 255 - col.getBlue()); - - input.setRGB(x, y, col.getRGB()); - - } - } - - } - - public static void saveBufferedImageFileChooser(Component parent, BufferedImage bufferedImage, EOCVSim eocvSim) { - - FileNameExtensionFilter jpegFilter = new FileNameExtensionFilter("JPEG (*.jpg)", "jpg", "jpeg"); - FileNameExtensionFilter pngFilter = new FileNameExtensionFilter("PNG (*.png)", "png"); - - String[] validExts = {"jpg", "jpeg", "png"}; - - DialogFactory.createFileChooser(parent, DialogFactory.FileChooser.Mode.SAVE_FILE_SELECT, jpegFilter, pngFilter) - - .addCloseListener((MODE, selectedFile, selectedFileFilter) -> { - if (MODE == JFileChooser.APPROVE_OPTION) { - - Optional extension = SysUtil.getExtensionByStringHandling(selectedFile.getName()); - - boolean saveImage; - - if (!selectedFile.exists()) { - saveImage = true; - } else { - FileAlreadyExists.UserChoice userChoice = DialogFactory.createFileAlreadyExistsDialog(eocvSim); //create confirm dialog - saveImage = userChoice == FileAlreadyExists.UserChoice.REPLACE; - } - - String ext = ""; - - if (saveImage) { - if (selectedFileFilter instanceof FileNameExtensionFilter) { //if user selected an extension - - //get selected extension - ext = ((FileNameExtensionFilter) selectedFileFilter).getExtensions()[0]; - - selectedFile = new File(selectedFile + "." + ext); //append extension to file - catchSaveBufferedImage(selectedFile, bufferedImage, ext); - - } else if (extension.isPresent() && Arrays.asList(validExts).contains(extension.get())) { //if user gave a extension to file and it's valid (jpg, jpeg or png) - - ext = extension.get(); //get the extension - catchSaveBufferedImage(selectedFile, bufferedImage, ext); - - } else { //default to jpg if the conditions are not met - - selectedFile = new File(selectedFile + ".jpg"); //append default extension to file - catchSaveBufferedImage(selectedFile, bufferedImage); - - } - } - - } - }); - - } - - public static void saveMatFileChooser(Component parent, Mat mat, EOCVSim eocvSim) { - - Mat clonedMat = mat.clone(); - - BufferedImage img = CvUtil.matToBufferedImage(clonedMat); - clonedMat.release(); - - saveBufferedImageFileChooser(parent, img, eocvSim); - - } - - public static ListModel isToListModel(InputStream is, Charset charset) throws UnsupportedEncodingException { - - DefaultListModel listModel = new DefaultListModel<>(); - String isStr = SysUtil.loadIsStr(is, charset); - - String[] lines = isStr.split("\n"); - - for(int i = 0 ; i < lines.length ; i++) { - listModel.add(i, lines[i]); - } - - return listModel; - - } - - public static class NoSelectionModel extends DefaultListSelectionModel { - - @Override - public void setAnchorSelectionIndex(final int anchorIndex) {} - - @Override - public void setLeadAnchorNotificationEnabled(final boolean flag) {} - - @Override - public void setLeadSelectionIndex(final int leadIndex) {} - - @Override - public void setSelectionInterval(final int index0, final int index1) { } - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.util; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.gui.DialogFactory; +import com.github.serivesmejia.eocvsim.gui.dialog.FileAlreadyExists; +import com.github.serivesmejia.eocvsim.util.CvUtil; +import com.github.serivesmejia.eocvsim.util.Log; +import com.github.serivesmejia.eocvsim.util.SysUtil; +import org.opencv.core.Mat; + +import javax.imageio.ImageIO; +import javax.swing.*; +import javax.swing.filechooser.FileNameExtensionFilter; +import javax.swing.text.AbstractDocument; +import javax.swing.text.AttributeSet; +import javax.swing.text.BadLocationException; +import javax.swing.text.DocumentFilter; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class GuiUtil { + + public static void jTextFieldOnlyNumbers(JTextField field, int minNumber, int onMinNumberChangeTo) { + + ((AbstractDocument) field.getDocument()).setDocumentFilter(new DocumentFilter() { + final Pattern regEx = Pattern.compile("\\d*"); + + @Override + public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException { + Matcher matcher = regEx.matcher(text); + if (!matcher.matches()) { + return; + } + + if (field.getText().length() == 0) { + try { + int number = Integer.parseInt(text); + if (number <= minNumber) { + text = String.valueOf(onMinNumberChangeTo); + } + } catch (NumberFormatException ex) { + } + } + + super.replace(fb, offset, length, text, attrs); + } + }); + + } + + public static ImageIcon scaleImage(ImageIcon icon, int w, int h) { + + int nw = icon.getIconWidth(); + int nh = icon.getIconHeight(); + + if (icon.getIconWidth() > w) { + nw = w; + nh = (nw * icon.getIconHeight()) / icon.getIconWidth(); + } + + if (nh > h) { + nh = h; + nw = (icon.getIconWidth() * nh) / icon.getIconHeight(); + } + + return new ImageIcon(icon.getImage().getScaledInstance(nw, nh, Image.SCALE_SMOOTH)); + + } + + public static ImageIcon loadImageIcon(String path) throws IOException { + return new ImageIcon(loadBufferedImage(path)); + } + + public static BufferedImage loadBufferedImage(String path) throws IOException { + return ImageIO.read(GuiUtil.class.getResourceAsStream(path)); + } + + public static void saveBufferedImage(File file, BufferedImage bufferedImage, String format) throws IOException { + ImageIO.write(bufferedImage, format, file); + } + + public static void saveBufferedImage(File file, BufferedImage bufferedImage) throws IOException { + saveBufferedImage(file, bufferedImage, "jpg"); + } + + public static void catchSaveBufferedImage(File file, BufferedImage bufferedImage, String format) { + try { + saveBufferedImage(file, bufferedImage, format); + } catch (IOException e) { + Log.error("GuiUtil", "Failed to save buffered image", e); + } + } + + public static void catchSaveBufferedImage(File file, BufferedImage bufferedImage) { + catchSaveBufferedImage(file, bufferedImage, "jpg"); + } + + public static void invertBufferedImageColors(BufferedImage input) { + + for (int x = 0; x < input.getWidth(); x++) { + for (int y = 0; y < input.getHeight(); y++) { + + int rgba = input.getRGB(x, y); + Color col = new Color(rgba, true); + + if (col.getAlpha() <= 0) continue; + + col = new Color(255 - col.getRed(), + 255 - col.getGreen(), + 255 - col.getBlue()); + + input.setRGB(x, y, col.getRGB()); + + } + } + + } + + public static void saveBufferedImageFileChooser(Component parent, BufferedImage bufferedImage, EOCVSim eocvSim) { + + FileNameExtensionFilter jpegFilter = new FileNameExtensionFilter("JPEG (*.jpg)", "jpg", "jpeg"); + FileNameExtensionFilter pngFilter = new FileNameExtensionFilter("PNG (*.png)", "png"); + + String[] validExts = {"jpg", "jpeg", "png"}; + + DialogFactory.createFileChooser(parent, DialogFactory.FileChooser.Mode.SAVE_FILE_SELECT, jpegFilter, pngFilter) + + .addCloseListener((MODE, selectedFile, selectedFileFilter) -> { + if (MODE == JFileChooser.APPROVE_OPTION) { + + Optional extension = SysUtil.getExtensionByStringHandling(selectedFile.getName()); + + boolean saveImage; + + if (!selectedFile.exists()) { + saveImage = true; + } else { + FileAlreadyExists.UserChoice userChoice = DialogFactory.createFileAlreadyExistsDialog(eocvSim); //create confirm dialog + saveImage = userChoice == FileAlreadyExists.UserChoice.REPLACE; + } + + String ext = ""; + + if (saveImage) { + if (selectedFileFilter instanceof FileNameExtensionFilter) { //if user selected an extension + + //get selected extension + ext = ((FileNameExtensionFilter) selectedFileFilter).getExtensions()[0]; + + selectedFile = new File(selectedFile + "." + ext); //append extension to file + catchSaveBufferedImage(selectedFile, bufferedImage, ext); + + } else if (extension.isPresent() && Arrays.asList(validExts).contains(extension.get())) { //if user gave a extension to file and it's valid (jpg, jpeg or png) + + ext = extension.get(); //get the extension + catchSaveBufferedImage(selectedFile, bufferedImage, ext); + + } else { //default to jpg if the conditions are not met + + selectedFile = new File(selectedFile + ".jpg"); //append default extension to file + catchSaveBufferedImage(selectedFile, bufferedImage); + + } + } + + } + }); + + } + + public static void saveMatFileChooser(Component parent, Mat mat, EOCVSim eocvSim) { + + Mat clonedMat = mat.clone(); + + BufferedImage img = CvUtil.matToBufferedImage(clonedMat); + clonedMat.release(); + + saveBufferedImageFileChooser(parent, img, eocvSim); + + } + + public static ListModel isToListModel(InputStream is, Charset charset) throws UnsupportedEncodingException { + + DefaultListModel listModel = new DefaultListModel<>(); + String isStr = SysUtil.loadIsStr(is, charset); + + String[] lines = isStr.split("\n"); + + for(int i = 0 ; i < lines.length ; i++) { + listModel.add(i, lines[i]); + } + + return listModel; + + } + + public static class NoSelectionModel extends DefaultListSelectionModel { + + @Override + public void setAnchorSelectionIndex(final int anchorIndex) {} + + @Override + public void setLeadAnchorNotificationEnabled(final boolean flag) {} + + @Override + public void setLeadSelectionIndex(final int leadIndex) {} + + @Override + public void setSelectionInterval(final int index0, final int index1) { } + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/MatPoster.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/MatPoster.java index f761e2ab..d8f93ef3 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/MatPoster.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/MatPoster.java @@ -1,216 +1,216 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.util; - -import com.github.serivesmejia.eocvsim.util.Log; -import com.github.serivesmejia.eocvsim.util.fps.FpsCounter; -import org.firstinspires.ftc.robotcore.internal.collections.EvictingBlockingQueue; -import org.opencv.core.Mat; -import org.openftc.easyopencv.MatRecycler; - -import java.util.ArrayList; -import java.util.concurrent.ArrayBlockingQueue; - -public class MatPoster { - - private final ArrayList postables = new ArrayList<>(); - - private final EvictingBlockingQueue postQueue; - private final MatRecycler matRecycler; - - private final String name; - - private final Thread posterThread; - - public final FpsCounter fpsCounter = new FpsCounter(); - - private final Object lock = new Object(); - - private volatile boolean paused = false; - - private volatile boolean hasPosterThreadStarted = false; - - public static MatPoster createWithoutRecycler(String name, int maxQueueItems) { - return new MatPoster(name, maxQueueItems, null); - } - - public MatPoster(String name, int maxQueueItems) { - this(name, new MatRecycler(maxQueueItems + 2)); - } - - public MatPoster(String name, MatRecycler recycler) { - this(name, recycler.getSize(), recycler); - } - - public MatPoster(String name, int maxQueueItems, MatRecycler recycler) { - postQueue = new EvictingBlockingQueue<>(new ArrayBlockingQueue<>(maxQueueItems)); - matRecycler = recycler; - posterThread = new Thread(new PosterRunnable(), "MatPoster-" + name + "-Thread"); - - this.name = name; - - postQueue.setEvictAction(this::evict); //release mat and return it to recycler if it's dropped by the EvictingBlockingQueue - } - - public void post(Mat m) { - if (m == null || m.empty()) { - Log.warn("MatPoster-" + name, "Tried to post empty or null mat, skipped this frame."); - return; - } - - if (matRecycler != null) { - if(matRecycler.getAvailableMatsAmount() < 1) { - //evict one if we don't have any available mats in the recycler - evict(postQueue.poll()); - } - - MatRecycler.RecyclableMat recycledMat = matRecycler.takeMat(); - m.copyTo(recycledMat); - - postQueue.offer(recycledMat); - } else { - postQueue.offer(m); - } - } - - public void synchronizedPost(Mat m) { - synchronize(() -> post(m)); - } - - public Mat pull() throws InterruptedException { - synchronized(lock) { - return postQueue.take(); - } - } - - public void clearQueue() { - if(postQueue.size() == 0) return; - - synchronized(lock) { - postQueue.clear(); - } - } - - public void synchronize(Runnable runn) { - synchronized(lock) { - runn.run(); - } - } - - public void addPostable(Postable postable) { - //start mat posting thread if it hasn't been started yet - if (!posterThread.isAlive() && !hasPosterThreadStarted) { - posterThread.start(); - } - - postables.add(postable); - } - - public void stop() { - Log.info("MatPoster-" + name, "Destroying..."); - - posterThread.interrupt(); - - for (Mat m : postQueue) { - if (m != null) { - if(m instanceof MatRecycler.RecyclableMat) { - ((MatRecycler.RecyclableMat)m).returnMat(); - } - } - } - - matRecycler.releaseAll(); - } - - private void evict(Mat m) { - if (m instanceof MatRecycler.RecyclableMat) { - ((MatRecycler.RecyclableMat) m).returnMat(); - } - m.release(); - } - - public void setPaused(boolean paused) { - this.paused = paused; - } - - public boolean getPaused() { - synchronized(lock) { - return paused; - } - } - - public String getName() { - return name; - } - - public interface Postable { - void post(Mat m); - } - - private class PosterRunnable implements Runnable { - - private Mat postableMat = new Mat(); - - @Override - public void run() { - hasPosterThreadStarted = true; - - while (!Thread.interrupted()) { - - while(paused && !Thread.currentThread().isInterrupted()) { - Thread.yield(); - } - - if (postQueue.size() == 0 || postables.size() == 0) continue; //skip if we have no queued frames - - synchronized(lock) { - fpsCounter.update(); - - try { - Mat takenMat = postQueue.take(); - - for (Postable postable : postables) { - takenMat.copyTo(postableMat); - postable.post(postableMat); - } - - takenMat.release(); - - if (takenMat instanceof MatRecycler.RecyclableMat) { - ((MatRecycler.RecyclableMat) takenMat).returnMat(); - } - } catch (InterruptedException e) { - e.printStackTrace(); - break; - } catch (Exception ex) { } - } - - } - - Log.warn("MatPoster-" + name +"-Thread", "Thread interrupted (" + Integer.toHexString(hashCode()) + ")"); - - } - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.util; + +import com.github.serivesmejia.eocvsim.util.Log; +import com.github.serivesmejia.eocvsim.util.fps.FpsCounter; +import org.firstinspires.ftc.robotcore.internal.collections.EvictingBlockingQueue; +import org.opencv.core.Mat; +import org.openftc.easyopencv.MatRecycler; + +import java.util.ArrayList; +import java.util.concurrent.ArrayBlockingQueue; + +public class MatPoster { + + private final ArrayList postables = new ArrayList<>(); + + private final EvictingBlockingQueue postQueue; + private final MatRecycler matRecycler; + + private final String name; + + private final Thread posterThread; + + public final FpsCounter fpsCounter = new FpsCounter(); + + private final Object lock = new Object(); + + private volatile boolean paused = false; + + private volatile boolean hasPosterThreadStarted = false; + + public static MatPoster createWithoutRecycler(String name, int maxQueueItems) { + return new MatPoster(name, maxQueueItems, null); + } + + public MatPoster(String name, int maxQueueItems) { + this(name, new MatRecycler(maxQueueItems + 2)); + } + + public MatPoster(String name, MatRecycler recycler) { + this(name, recycler.getSize(), recycler); + } + + public MatPoster(String name, int maxQueueItems, MatRecycler recycler) { + postQueue = new EvictingBlockingQueue<>(new ArrayBlockingQueue<>(maxQueueItems)); + matRecycler = recycler; + posterThread = new Thread(new PosterRunnable(), "MatPoster-" + name + "-Thread"); + + this.name = name; + + postQueue.setEvictAction(this::evict); //release mat and return it to recycler if it's dropped by the EvictingBlockingQueue + } + + public void post(Mat m) { + if (m == null || m.empty()) { + Log.warn("MatPoster-" + name, "Tried to post empty or null mat, skipped this frame."); + return; + } + + if (matRecycler != null) { + if(matRecycler.getAvailableMatsAmount() < 1) { + //evict one if we don't have any available mats in the recycler + evict(postQueue.poll()); + } + + MatRecycler.RecyclableMat recycledMat = matRecycler.takeMat(); + m.copyTo(recycledMat); + + postQueue.offer(recycledMat); + } else { + postQueue.offer(m); + } + } + + public void synchronizedPost(Mat m) { + synchronize(() -> post(m)); + } + + public Mat pull() throws InterruptedException { + synchronized(lock) { + return postQueue.take(); + } + } + + public void clearQueue() { + if(postQueue.size() == 0) return; + + synchronized(lock) { + postQueue.clear(); + } + } + + public void synchronize(Runnable runn) { + synchronized(lock) { + runn.run(); + } + } + + public void addPostable(Postable postable) { + //start mat posting thread if it hasn't been started yet + if (!posterThread.isAlive() && !hasPosterThreadStarted) { + posterThread.start(); + } + + postables.add(postable); + } + + public void stop() { + Log.info("MatPoster-" + name, "Destroying..."); + + posterThread.interrupt(); + + for (Mat m : postQueue) { + if (m != null) { + if(m instanceof MatRecycler.RecyclableMat) { + ((MatRecycler.RecyclableMat)m).returnMat(); + } + } + } + + matRecycler.releaseAll(); + } + + private void evict(Mat m) { + if (m instanceof MatRecycler.RecyclableMat) { + ((MatRecycler.RecyclableMat) m).returnMat(); + } + m.release(); + } + + public void setPaused(boolean paused) { + this.paused = paused; + } + + public boolean getPaused() { + synchronized(lock) { + return paused; + } + } + + public String getName() { + return name; + } + + public interface Postable { + void post(Mat m); + } + + private class PosterRunnable implements Runnable { + + private Mat postableMat = new Mat(); + + @Override + public void run() { + hasPosterThreadStarted = true; + + while (!Thread.interrupted()) { + + while(paused && !Thread.currentThread().isInterrupted()) { + Thread.yield(); + } + + if (postQueue.size() == 0 || postables.size() == 0) continue; //skip if we have no queued frames + + synchronized(lock) { + fpsCounter.update(); + + try { + Mat takenMat = postQueue.take(); + + for (Postable postable : postables) { + takenMat.copyTo(postableMat); + postable.post(postableMat); + } + + takenMat.release(); + + if (takenMat instanceof MatRecycler.RecyclableMat) { + ((MatRecycler.RecyclableMat) takenMat).returnMat(); + } + } catch (InterruptedException e) { + e.printStackTrace(); + break; + } catch (Exception ex) { } + } + + } + + Log.warn("MatPoster-" + name +"-Thread", "Thread interrupted (" + Integer.toHexString(hashCode()) + ")"); + + } + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ReflectTaskbar.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ReflectTaskbar.kt index e72609e3..f9c68161 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ReflectTaskbar.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ReflectTaskbar.kt @@ -1,33 +1,33 @@ -package com.github.serivesmejia.eocvsim.gui.util - -import java.awt.Image -import java.lang.reflect.InvocationTargetException - -object ReflectTaskbar { - - private val taskbarClass by lazy { Class.forName("java.awt.Taskbar") } - - private val isTaskbarSupportedMethod by lazy { taskbarClass.getDeclaredMethod("isTaskbarSupported") } - - private val getTaskbarMethod by lazy { taskbarClass.getDeclaredMethod("getTaskbar") } - private val setIconImageMethod by lazy { taskbarClass.getDeclaredMethod("setIconImage", Image::class.java) } - - val isUsable by lazy { - try { - isTaskbarSupported - } catch(ex: ClassNotFoundException) { false } - } - val isTaskbarSupported get() = isTaskbarSupportedMethod.invoke(null) as Boolean - - val taskbar by lazy { getTaskbarMethod.invoke(null) } - - @Throws(SecurityException::class, UnsupportedOperationException::class) - fun setIconImage(image: Image) { - try { - setIconImageMethod.invoke(taskbar, image) - } catch(e: InvocationTargetException) { - throw e.cause ?: e - } - } - +package com.github.serivesmejia.eocvsim.gui.util + +import java.awt.Image +import java.lang.reflect.InvocationTargetException + +object ReflectTaskbar { + + private val taskbarClass by lazy { Class.forName("java.awt.Taskbar") } + + private val isTaskbarSupportedMethod by lazy { taskbarClass.getDeclaredMethod("isTaskbarSupported") } + + private val getTaskbarMethod by lazy { taskbarClass.getDeclaredMethod("getTaskbar") } + private val setIconImageMethod by lazy { taskbarClass.getDeclaredMethod("setIconImage", Image::class.java) } + + val isUsable by lazy { + try { + isTaskbarSupported + } catch(ex: ClassNotFoundException) { false } + } + val isTaskbarSupported get() = isTaskbarSupportedMethod.invoke(null) as Boolean + + val taskbar by lazy { getTaskbarMethod.invoke(null) } + + @Throws(SecurityException::class, UnsupportedOperationException::class) + fun setIconImage(image: Image) { + try { + setIconImageMethod.invoke(taskbar, image) + } catch(e: InvocationTargetException) { + throw e.cause ?: e + } + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ValidCharactersDocumentFilter.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ValidCharactersDocumentFilter.kt index 24c957ae..cd589573 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ValidCharactersDocumentFilter.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/ValidCharactersDocumentFilter.kt @@ -1,69 +1,69 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.util - -import javax.swing.JTextField -import javax.swing.text.AttributeSet -import javax.swing.text.BadLocationException -import javax.swing.text.DocumentFilter - -class ValidCharactersDocumentFilter(val validCharacters: Array) : DocumentFilter() { - - @get:Synchronized - @Volatile var valid = false - private set - - @get:Synchronized - @Volatile var lastValid = 0.0 - private set - - @Volatile private var lastText = "" - - @Throws(BadLocationException::class) - @Synchronized override fun replace(fb: FilterBypass?, offset: Int, length: Int, text: String, attrs: AttributeSet?) { - val newText = text.replace(" ", "") - - for (c in newText.toCharArray()) { - if(!isValidCharacter(c)) return - } - - valid = try { - lastValid = newText.toDouble() - lastText = newText - newText != "" - } catch (ex: NumberFormatException) { - false - } - - super.replace(fb, offset, length, newText, attrs) - } - - private fun isValidCharacter(c: Char): Boolean { - for (validC in validCharacters) { - if (c == validC) return true - } - return false - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.util + +import javax.swing.JTextField +import javax.swing.text.AttributeSet +import javax.swing.text.BadLocationException +import javax.swing.text.DocumentFilter + +class ValidCharactersDocumentFilter(val validCharacters: Array) : DocumentFilter() { + + @get:Synchronized + @Volatile var valid = false + private set + + @get:Synchronized + @Volatile var lastValid = 0.0 + private set + + @Volatile private var lastText = "" + + @Throws(BadLocationException::class) + @Synchronized override fun replace(fb: FilterBypass?, offset: Int, length: Int, text: String, attrs: AttributeSet?) { + val newText = text.replace(" ", "") + + for (c in newText.toCharArray()) { + if(!isValidCharacter(c)) return + } + + valid = try { + lastValid = newText.toDouble() + lastText = newText + newText != "" + } catch (ex: NumberFormatException) { + false + } + + super.replace(fb, offset, length, newText, attrs) + } + + private fun isValidCharacter(c: Char): Boolean { + for (validC in validCharacters) { + if (c == validC) return true + } + return false + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/extension/SwingExt.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/extension/SwingExt.kt index e034e9b9..b5081f23 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/extension/SwingExt.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/extension/SwingExt.kt @@ -1,45 +1,45 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.util.extension - -import javax.swing.JTextField -import javax.swing.text.AbstractDocument -import javax.swing.text.DocumentFilter - -object SwingExt { - - val JTextField.abstractDocument: AbstractDocument - get() { - return (document as AbstractDocument) - } - - var JTextField.documentFilter: DocumentFilter - get() { - return abstractDocument.documentFilter - } - set(value) { - abstractDocument.documentFilter = value - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.util.extension + +import javax.swing.JTextField +import javax.swing.text.AbstractDocument +import javax.swing.text.DocumentFilter + +object SwingExt { + + val JTextField.abstractDocument: AbstractDocument + get() { + return (document as AbstractDocument) + } + + var JTextField.documentFilter: DocumentFilter + get() { + return abstractDocument.documentFilter + } + set(value) { + abstractDocument.documentFilter = value + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/PipelineListIconRenderer.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/PipelineListIconRenderer.kt index dd3d949f..94f6e107 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/PipelineListIconRenderer.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/PipelineListIconRenderer.kt @@ -1,45 +1,45 @@ -package com.github.serivesmejia.eocvsim.gui.util.icon - -import com.github.serivesmejia.eocvsim.gui.Icons -import com.github.serivesmejia.eocvsim.input.InputSourceManager -import com.github.serivesmejia.eocvsim.pipeline.PipelineManager -import com.github.serivesmejia.eocvsim.pipeline.PipelineSource - -import javax.swing.* -import java.awt.* - -class PipelineListIconRenderer( - private val pipelineManager: PipelineManager -) : DefaultListCellRenderer() { - - private val gearsIcon by Icons.lazyGetImageResized("ico_gears", 15, 15) - private val hammerIcon by Icons.lazyGetImageResized("ico_hammer", 15, 15) - - override fun getListCellRendererComponent( - list: JList<*>, - value: Any, - index: Int, - isSelected: Boolean, - cellHasFocus: Boolean - ): Component { - val label = super.getListCellRendererComponent( - list, value, index, isSelected, cellHasFocus - ) as JLabel - - val runtimePipelinesAmount = pipelineManager.getPipelinesFrom( - PipelineSource.COMPILED_ON_RUNTIME - ).size - - if(runtimePipelinesAmount > 0) { - val source = pipelineManager.pipelines[index].source - - label.icon = when(source) { - PipelineSource.COMPILED_ON_RUNTIME -> gearsIcon - else -> hammerIcon - } - } - - return label - } - -} +package com.github.serivesmejia.eocvsim.gui.util.icon + +import com.github.serivesmejia.eocvsim.gui.Icons +import com.github.serivesmejia.eocvsim.input.InputSourceManager +import com.github.serivesmejia.eocvsim.pipeline.PipelineManager +import com.github.serivesmejia.eocvsim.pipeline.PipelineSource + +import javax.swing.* +import java.awt.* + +class PipelineListIconRenderer( + private val pipelineManager: PipelineManager +) : DefaultListCellRenderer() { + + private val gearsIcon by Icons.lazyGetImageResized("ico_gears", 15, 15) + private val hammerIcon by Icons.lazyGetImageResized("ico_hammer", 15, 15) + + override fun getListCellRendererComponent( + list: JList<*>, + value: Any, + index: Int, + isSelected: Boolean, + cellHasFocus: Boolean + ): Component { + val label = super.getListCellRendererComponent( + list, value, index, isSelected, cellHasFocus + ) as JLabel + + val runtimePipelinesAmount = pipelineManager.getPipelinesFrom( + PipelineSource.COMPILED_ON_RUNTIME + ).size + + if(runtimePipelinesAmount > 0) { + val source = pipelineManager.pipelines[index].source + + label.icon = when(source) { + PipelineSource.COMPILED_ON_RUNTIME -> gearsIcon + else -> hammerIcon + } + } + + return label + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/SourcesListIconRenderer.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/SourcesListIconRenderer.java index 0795e245..dd582038 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/SourcesListIconRenderer.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/util/icon/SourcesListIconRenderer.java @@ -1,84 +1,84 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.gui.util.icon; - -import com.github.serivesmejia.eocvsim.gui.Icons; -import com.github.serivesmejia.eocvsim.input.InputSourceManager; - -import javax.swing.*; -import java.awt.*; - -public class SourcesListIconRenderer extends DefaultListCellRenderer { - - public static final int ICO_W = 15; - public static final int ICO_H = 15; - - public InputSourceManager sourceManager = null; - - ImageIcon imageIcon = null; - ImageIcon camIcon = null; - ImageIcon vidIcon = null; - - public SourcesListIconRenderer(InputSourceManager sourceManager) { - this.sourceManager = sourceManager; - } - - @Override - public Component getListCellRendererComponent( - JList list, - Object value, - int index, - boolean isSelected, - boolean cellHasFocus) { - - // Get the renderer component from parent class - JLabel label = (JLabel) super.getListCellRendererComponent(list, - value, index, isSelected, cellHasFocus); - - switch (sourceManager.getSourceType((String) value)) { - case IMAGE: - if(imageIcon == null) { - imageIcon = Icons.INSTANCE.getImageResized("ico_img", 15, 15); - } - label.setIcon(imageIcon); - break; - case CAMERA: - if(camIcon == null) { - camIcon = Icons.INSTANCE.getImageResized("ico_cam", 15, 15); - } - label.setIcon(camIcon); - break; - case VIDEO: - if(vidIcon == null) { - vidIcon = Icons.INSTANCE.getImageResized("ico_vid", 15, 15); - } - label.setIcon(vidIcon); - break; - } - - return label; - - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.gui.util.icon; + +import com.github.serivesmejia.eocvsim.gui.Icons; +import com.github.serivesmejia.eocvsim.input.InputSourceManager; + +import javax.swing.*; +import java.awt.*; + +public class SourcesListIconRenderer extends DefaultListCellRenderer { + + public static final int ICO_W = 15; + public static final int ICO_H = 15; + + public InputSourceManager sourceManager = null; + + ImageIcon imageIcon = null; + ImageIcon camIcon = null; + ImageIcon vidIcon = null; + + public SourcesListIconRenderer(InputSourceManager sourceManager) { + this.sourceManager = sourceManager; + } + + @Override + public Component getListCellRendererComponent( + JList list, + Object value, + int index, + boolean isSelected, + boolean cellHasFocus) { + + // Get the renderer component from parent class + JLabel label = (JLabel) super.getListCellRendererComponent(list, + value, index, isSelected, cellHasFocus); + + switch (sourceManager.getSourceType((String) value)) { + case IMAGE: + if(imageIcon == null) { + imageIcon = Icons.INSTANCE.getImageResized("ico_img", 15, 15); + } + label.setIcon(imageIcon); + break; + case CAMERA: + if(camIcon == null) { + camIcon = Icons.INSTANCE.getImageResized("ico_cam", 15, 15); + } + label.setIcon(camIcon); + break; + case VIDEO: + if(vidIcon == null) { + vidIcon = Icons.INSTANCE.getImageResized("ico_vid", 15, 15); + } + label.setIcon(vidIcon); + break; + } + + return label; + + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.java index b71fec57..7fbcb034 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSource.java @@ -1,90 +1,90 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.input; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import org.opencv.core.Mat; - -import javax.swing.filechooser.FileFilter; - -public abstract class InputSource implements Comparable { - - public transient boolean isDefault = false; - public transient EOCVSim eocvSim = null; - - protected transient String name = ""; - protected transient boolean isPaused = false; - private transient boolean beforeIsPaused = false; - - protected long createdOn = -1L; - - public abstract boolean init(); - public abstract void reset(); - public abstract void close(); - - public abstract void onPause(); - public abstract void onResume(); - - public Mat update() { - return null; - } - - public final InputSource cloneSource() { - InputSource source = internalCloneSource(); - source.createdOn = createdOn; - return source; - } - - protected abstract InputSource internalCloneSource(); - - public final void setPaused(boolean paused) { - - isPaused = paused; - - if (beforeIsPaused != isPaused) { - if (isPaused) { - onPause(); - } else { - onResume(); - } - } - - beforeIsPaused = paused; - - } - - public final String getName() { - return name; - } - - public abstract FileFilter getFileFilters(); - - public abstract long getCaptureTimeNanos(); - - @Override - public final int compareTo(InputSource source) { - return createdOn > source.createdOn ? 1 : -1; - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.input; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import org.opencv.core.Mat; + +import javax.swing.filechooser.FileFilter; + +public abstract class InputSource implements Comparable { + + public transient boolean isDefault = false; + public transient EOCVSim eocvSim = null; + + protected transient String name = ""; + protected transient boolean isPaused = false; + private transient boolean beforeIsPaused = false; + + protected long createdOn = -1L; + + public abstract boolean init(); + public abstract void reset(); + public abstract void close(); + + public abstract void onPause(); + public abstract void onResume(); + + public Mat update() { + return null; + } + + public final InputSource cloneSource() { + InputSource source = internalCloneSource(); + source.createdOn = createdOn; + return source; + } + + protected abstract InputSource internalCloneSource(); + + public final void setPaused(boolean paused) { + + isPaused = paused; + + if (beforeIsPaused != isPaused) { + if (isPaused) { + onPause(); + } else { + onResume(); + } + } + + beforeIsPaused = paused; + + } + + public final String getName() { + return name; + } + + public abstract FileFilter getFileFilters(); + + public abstract long getCaptureTimeNanos(); + + @Override + public final int compareTo(InputSource source) { + return createdOn > source.createdOn ? 1 : -1; + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceLoader.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceLoader.java index 9f4eccf5..f2be987f 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceLoader.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceLoader.java @@ -1,183 +1,183 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.input; - -import com.github.serivesmejia.eocvsim.input.source.CameraSource; -import com.github.serivesmejia.eocvsim.input.source.ImageSource; -import com.github.serivesmejia.eocvsim.input.source.VideoSource; -import com.github.serivesmejia.eocvsim.util.Log; -import com.github.serivesmejia.eocvsim.util.SysUtil; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.annotations.Expose; - -import java.io.File; -import java.util.HashMap; -import java.util.Map; - -public class InputSourceLoader { - - public static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); - - public static final String SOURCES_SAVEFILE_NAME = "eocvsim_sources.json"; - - public static final File SOURCES_SAVEFILE = new File(SysUtil.getEOCVSimFolder() + File.separator + SOURCES_SAVEFILE_NAME); - public static final File SOURCES_SAVEFILE_OLD = new File(SysUtil.getAppData() + File.separator + SOURCES_SAVEFILE_NAME); - - public static final InputSourcesContainer.SourcesFileVersion CURRENT_FILE_VERSION = InputSourcesContainer.SourcesFileVersion.SEIS; - - public HashMap loadedInputSources = new HashMap<>(); - - public InputSourcesContainer.SourcesFileVersion fileVersion = null; - - public void saveInputSource(String name, InputSource source) { - loadedInputSources.put(name, source); - } - - public void deleteInputSource(String name) { - loadedInputSources.remove(name); - } - - public void saveInputSourcesToFile() { - saveInputSourcesToFile(SOURCES_SAVEFILE); - } - - public void saveInputSourcesToFile(File f) { - - InputSourcesContainer sourcesContainer = new InputSourcesContainer(); - - //updates file version to most recent since it will be regenerated at this point - if(fileVersion != null) - sourcesContainer.sourcesFileVersion = fileVersion.ordinal() < CURRENT_FILE_VERSION.ordinal() - ? CURRENT_FILE_VERSION : fileVersion; - - for (Map.Entry entry : loadedInputSources.entrySet()) { - if (!entry.getValue().isDefault) { - InputSource source = entry.getValue().cloneSource(); - sourcesContainer.classifySource(entry.getKey(), source); - } - } - - saveInputSourcesToFile(f, sourcesContainer); - - } - - public void saveInputSourcesToFile(File file, InputSourcesContainer sourcesContainer) { - String jsonInputSources = gson.toJson(sourcesContainer); - SysUtil.saveFileStr(file, jsonInputSources); - } - - public void saveInputSourcesToFile(InputSourcesContainer sourcesContainer) { - saveInputSourcesToFile(SOURCES_SAVEFILE, sourcesContainer); - } - - public void loadInputSourcesFromFile() { - SysUtil.migrateFile(SOURCES_SAVEFILE_OLD, SOURCES_SAVEFILE); - loadInputSourcesFromFile(SOURCES_SAVEFILE); - } - - public void loadInputSourcesFromFile(File f) { - - if (!f.exists()) return; - - String jsonSources = SysUtil.loadFileStr(f); - if (jsonSources.trim().equals("")) return; - - InputSourcesContainer sources; - - try { - sources = gson.fromJson(jsonSources, InputSourcesContainer.class); - } catch (Exception ex) { - Log.error("InputSourceLoader", "Error while parsing sources file, it will be replaced and fixed later on, but the user created sources will be deleted.", ex); - Log.blank(); - return; - } - - sources.updateAllSources(); - fileVersion = sources.sourcesFileVersion; - - saveInputSourcesToFile(sources); //to make sure version gets declared in case it was an older file - - Log.info("InputSourceLoader", "InputSources file version is " + sources.sourcesFileVersion); - - loadedInputSources = sources.allSources; - - } - - static class InputSourcesContainer { - - public transient HashMap allSources = new HashMap<>(); - - public HashMap imageSources = new HashMap<>(); - public HashMap cameraSources = new HashMap<>(); - public HashMap videoSources = new HashMap<>(); - - @Expose - public SourcesFileVersion sourcesFileVersion = null; - - enum SourcesFileVersion { DOS, SEIS, SIETE } - - public void updateAllSources() { - - if(sourcesFileVersion == null) sourcesFileVersion = SourcesFileVersion.DOS; - - allSources.clear(); - - for (Map.Entry entry : imageSources.entrySet()) { - allSources.put(entry.getKey(), entry.getValue()); - } - - for (Map.Entry entry : cameraSources.entrySet()) { - allSources.put(entry.getKey(), entry.getValue()); - } - - //check if file version is bigger than DOS, we should have video sources section - //declared in any file with a version greater than that - if(sourcesFileVersion.ordinal() >= 1) { - for (Map.Entry entry : videoSources.entrySet()) { - allSources.put(entry.getKey(), entry.getValue()); - } - } - - } - - public void classifySource(String sourceName, InputSource source) { - - switch (SourceType.fromClass(source.getClass())) { - case IMAGE: - imageSources.put(sourceName, (ImageSource) source); - break; - case CAMERA: - cameraSources.put(sourceName, (CameraSource) source); - break; - case VIDEO: - videoSources.put(sourceName, (VideoSource) source); - break; - } - - } - - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.input; + +import com.github.serivesmejia.eocvsim.input.source.CameraSource; +import com.github.serivesmejia.eocvsim.input.source.ImageSource; +import com.github.serivesmejia.eocvsim.input.source.VideoSource; +import com.github.serivesmejia.eocvsim.util.Log; +import com.github.serivesmejia.eocvsim.util.SysUtil; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.Expose; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +public class InputSourceLoader { + + public static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + public static final String SOURCES_SAVEFILE_NAME = "eocvsim_sources.json"; + + public static final File SOURCES_SAVEFILE = new File(SysUtil.getEOCVSimFolder() + File.separator + SOURCES_SAVEFILE_NAME); + public static final File SOURCES_SAVEFILE_OLD = new File(SysUtil.getAppData() + File.separator + SOURCES_SAVEFILE_NAME); + + public static final InputSourcesContainer.SourcesFileVersion CURRENT_FILE_VERSION = InputSourcesContainer.SourcesFileVersion.SEIS; + + public HashMap loadedInputSources = new HashMap<>(); + + public InputSourcesContainer.SourcesFileVersion fileVersion = null; + + public void saveInputSource(String name, InputSource source) { + loadedInputSources.put(name, source); + } + + public void deleteInputSource(String name) { + loadedInputSources.remove(name); + } + + public void saveInputSourcesToFile() { + saveInputSourcesToFile(SOURCES_SAVEFILE); + } + + public void saveInputSourcesToFile(File f) { + + InputSourcesContainer sourcesContainer = new InputSourcesContainer(); + + //updates file version to most recent since it will be regenerated at this point + if(fileVersion != null) + sourcesContainer.sourcesFileVersion = fileVersion.ordinal() < CURRENT_FILE_VERSION.ordinal() + ? CURRENT_FILE_VERSION : fileVersion; + + for (Map.Entry entry : loadedInputSources.entrySet()) { + if (!entry.getValue().isDefault) { + InputSource source = entry.getValue().cloneSource(); + sourcesContainer.classifySource(entry.getKey(), source); + } + } + + saveInputSourcesToFile(f, sourcesContainer); + + } + + public void saveInputSourcesToFile(File file, InputSourcesContainer sourcesContainer) { + String jsonInputSources = gson.toJson(sourcesContainer); + SysUtil.saveFileStr(file, jsonInputSources); + } + + public void saveInputSourcesToFile(InputSourcesContainer sourcesContainer) { + saveInputSourcesToFile(SOURCES_SAVEFILE, sourcesContainer); + } + + public void loadInputSourcesFromFile() { + SysUtil.migrateFile(SOURCES_SAVEFILE_OLD, SOURCES_SAVEFILE); + loadInputSourcesFromFile(SOURCES_SAVEFILE); + } + + public void loadInputSourcesFromFile(File f) { + + if (!f.exists()) return; + + String jsonSources = SysUtil.loadFileStr(f); + if (jsonSources.trim().equals("")) return; + + InputSourcesContainer sources; + + try { + sources = gson.fromJson(jsonSources, InputSourcesContainer.class); + } catch (Exception ex) { + Log.error("InputSourceLoader", "Error while parsing sources file, it will be replaced and fixed later on, but the user created sources will be deleted.", ex); + Log.blank(); + return; + } + + sources.updateAllSources(); + fileVersion = sources.sourcesFileVersion; + + saveInputSourcesToFile(sources); //to make sure version gets declared in case it was an older file + + Log.info("InputSourceLoader", "InputSources file version is " + sources.sourcesFileVersion); + + loadedInputSources = sources.allSources; + + } + + static class InputSourcesContainer { + + public transient HashMap allSources = new HashMap<>(); + + public HashMap imageSources = new HashMap<>(); + public HashMap cameraSources = new HashMap<>(); + public HashMap videoSources = new HashMap<>(); + + @Expose + public SourcesFileVersion sourcesFileVersion = null; + + enum SourcesFileVersion { DOS, SEIS, SIETE } + + public void updateAllSources() { + + if(sourcesFileVersion == null) sourcesFileVersion = SourcesFileVersion.DOS; + + allSources.clear(); + + for (Map.Entry entry : imageSources.entrySet()) { + allSources.put(entry.getKey(), entry.getValue()); + } + + for (Map.Entry entry : cameraSources.entrySet()) { + allSources.put(entry.getKey(), entry.getValue()); + } + + //check if file version is bigger than DOS, we should have video sources section + //declared in any file with a version greater than that + if(sourcesFileVersion.ordinal() >= 1) { + for (Map.Entry entry : videoSources.entrySet()) { + allSources.put(entry.getKey(), entry.getValue()); + } + } + + } + + public void classifySource(String sourceName, InputSource source) { + + switch (SourceType.fromClass(source.getClass())) { + case IMAGE: + imageSources.put(sourceName, (ImageSource) source); + break; + case CAMERA: + cameraSources.put(sourceName, (CameraSource) source); + break; + case VIDEO: + videoSources.put(sourceName, (VideoSource) source); + break; + } + + } + + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java index 5ba73955..3b848fe9 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java @@ -1,292 +1,292 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.input; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.gui.Visualizer; -import com.github.serivesmejia.eocvsim.gui.component.visualizer.SourceSelectorPanel; -import com.github.serivesmejia.eocvsim.input.source.ImageSource; -import com.github.serivesmejia.eocvsim.pipeline.PipelineManager; -import com.github.serivesmejia.eocvsim.util.Log; -import com.github.serivesmejia.eocvsim.util.SysUtil; -import org.opencv.core.Mat; -import org.opencv.core.Size; - -import javax.swing.*; -import java.awt.*; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.*; - -public class InputSourceManager { - - private final EOCVSim eocvSim; - - public volatile Mat lastMatFromSource = null; - public volatile InputSource currentInputSource = null; - - public volatile HashMap sources = new HashMap<>(); - - public InputSourceLoader inputSourceLoader = new InputSourceLoader(); - public SourceSelectorPanel selectorPanel; - - public InputSourceManager(EOCVSim eocvSim) { - this.eocvSim = eocvSim; - selectorPanel = eocvSim.visualizer.sourceSelectorPanel; - } - - public void init() { - - Log.info("InputSourceManager", "Initializing..."); - - if(lastMatFromSource == null) - lastMatFromSource = new Mat(); - - Size size = new Size(320, 240); - createDefaultImgInputSource("/images/ug_4.jpg", "ug_eocvsim_4.jpg", "Ultimate Goal 4 Ring", size); - createDefaultImgInputSource("/images/ug_1.jpg", "ug_eocvsim_1.jpg", "Ultimate Goal 1 Ring", size); - createDefaultImgInputSource("/images/ug_0.jpg", "ug_eocvsim_0.jpg", "Ultimate Goal 0 Ring", size); - - setInputSource("Ultimate Goal 4 Ring"); - - inputSourceLoader.loadInputSourcesFromFile(); - - for (Map.Entry entry : inputSourceLoader.loadedInputSources.entrySet()) { - addInputSource(entry.getKey(), entry.getValue()); - } - - Log.blank(); - } - - private void createDefaultImgInputSource(String resourcePath, String fileName, String sourceName, Size imgSize) { - try { - - InputStream is = InputSource.class.getResourceAsStream(resourcePath); - File f = SysUtil.copyFileIsTemp(is, fileName, true).file; - - ImageSource src = new ImageSource(f.getAbsolutePath(), imgSize); - src.isDefault = true; - src.createdOn = sources.size(); - - addInputSource(sourceName, src); - - } catch (IOException e) { - e.printStackTrace(); - } - } - - public void update(boolean isPaused) { - if(currentInputSource == null) return; - currentInputSource.setPaused(isPaused); - - try { - Mat m = currentInputSource.update(); - if(m != null && !m.empty()) m.copyTo(lastMatFromSource); - } catch(Exception ex) { - Log.error("InputSourceManager", "Error while processing current source", ex); - } - } - - - public void addInputSource(String name, InputSource inputSource) { - addInputSource(name, inputSource, false); - } - - public void addInputSource(String name, InputSource inputSource, boolean dispatchedByUser) { - if (inputSource == null) { - return; - } - - if (sources.containsKey(name)) return; - - if(eocvSim.visualizer.sourceSelectorPanel != null) { - eocvSim.visualizer.sourceSelectorPanel.setAllowSourceSwitching(false); - } - inputSource.name = name; - - sources.put(name, inputSource); - - if(inputSource.createdOn == -1) - inputSource.createdOn = System.currentTimeMillis(); - - if(!inputSource.isDefault) { - inputSourceLoader.saveInputSource(name, inputSource); - inputSourceLoader.saveInputSourcesToFile(); - } - - if(eocvSim.visualizer.sourceSelectorPanel != null) { - SourceSelectorPanel selectorPanel = eocvSim.visualizer.sourceSelectorPanel; - - selectorPanel.updateSourcesList(); - - SwingUtilities.invokeLater(() -> { - JList sourceSelector = selectorPanel.getSourceSelector(); - - int currentSourceIndex = sourceSelector.getSelectedIndex(); - - if(dispatchedByUser) { - int index = selectorPanel.getIndexOf(name); - - sourceSelector.setSelectedIndex(index); - - requestSetInputSource(name); - - eocvSim.onMainUpdate.doOnce(() -> { - eocvSim.pipelineManager.requestSetPaused(false); - pauseIfImageTwoFrames(); - }); - } else { - sourceSelector.setSelectedIndex(currentSourceIndex); - } - - selectorPanel.setAllowSourceSwitching(true); - }); - } - - Log.info("InputSourceManager", "Adding InputSource " + inputSource.toString() + " (" + inputSource.getClass().getSimpleName() + ")"); - } - - public void deleteInputSource(String sourceName) { - InputSource src = sources.get(sourceName); - - if (src == null) return; - if (src.isDefault) return; - - sources.remove(sourceName); - - inputSourceLoader.deleteInputSource(sourceName); - inputSourceLoader.saveInputSourcesToFile(); - } - - public boolean setInputSource(String sourceName) { - InputSource src = sources.get(sourceName); - - if (src != null) { - src.reset(); - src.eocvSim = eocvSim; - } - - //check if source type is a camera, and if so, create a please wait dialog - Visualizer.AsyncPleaseWaitDialog apwd = showApwdIfNeeded(sourceName); - - if (src != null) { - if (!src.init()) { - if (apwd != null) { - apwd.destroyDialog(); - } - - eocvSim.visualizer.asyncPleaseWaitDialog("Error while loading requested source", "Falling back to previous source", - "Close", new Dimension(300, 150), true, true); - - Log.error("InputSourceManager", "Error while loading requested source (" + sourceName + ") reported by itself (init method returned false)"); - - return false; - } - } - - //if there's a please wait dialog for a camera source, destroy it. - if (apwd != null) { - apwd.destroyDialog(); - } - - if (currentInputSource != null) { - currentInputSource.reset(); - } - - currentInputSource = src; - - //if pause on images option is turned on by user - if (eocvSim.configManager.getConfig().pauseOnImages) - pauseIfImage(); - - Log.info("InputSourceManager", "Set InputSource to " + currentInputSource.toString() + " (" + src.getClass().getSimpleName() + ")"); - - return true; - - } - - public boolean isNameOnUse(String name) { - return sources.containsKey(name); - } - - public void pauseIfImage() { - //if the new input source is an image, we will pause the next frame - //to execute one shot analysis on images and save resources. - if (SourceType.fromClass(currentInputSource.getClass()) == SourceType.IMAGE) { - eocvSim.onMainUpdate.doOnce(() -> - eocvSim.pipelineManager.setPaused( - true, - PipelineManager.PauseReason.IMAGE_ONE_ANALYSIS - ) - ); - } - } - - public void pauseIfImageTwoFrames() { - //if the new input source is an image, we will pause the next frame - //to execute one shot analysis on images and save resources. - eocvSim.onMainUpdate.doOnce(this::pauseIfImage); - } - - public void requestSetInputSource(String name) { - eocvSim.onMainUpdate.doOnce(() -> setInputSource(name)); - } - - public Visualizer.AsyncPleaseWaitDialog showApwdIfNeeded(String sourceName) { - Visualizer.AsyncPleaseWaitDialog apwd = null; - - if (getSourceType(sourceName) == SourceType.CAMERA || getSourceType(sourceName) == SourceType.VIDEO) { - apwd = eocvSim.visualizer.asyncPleaseWaitDialog( - "Opening source...", null, "Exit", - new Dimension(300, 150), true - ); - - apwd.onCancel(eocvSim::destroy); - } - - return apwd; - } - - public SourceType getSourceType(String sourceName) { - if(sourceName == null) { - return SourceType.UNKNOWN; - } - - InputSource source = sources.get(sourceName); - - if(source == null) { - return SourceType.UNKNOWN; - } - return SourceType.fromClass(source.getClass()); - } - - public InputSource[] getSortedInputSources() { - ArrayList sources = new ArrayList<>(this.sources.values()); - Collections.sort(sources); - - return sources.toArray(new InputSource[0]); - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.input; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.gui.Visualizer; +import com.github.serivesmejia.eocvsim.gui.component.visualizer.SourceSelectorPanel; +import com.github.serivesmejia.eocvsim.input.source.ImageSource; +import com.github.serivesmejia.eocvsim.pipeline.PipelineManager; +import com.github.serivesmejia.eocvsim.util.Log; +import com.github.serivesmejia.eocvsim.util.SysUtil; +import org.opencv.core.Mat; +import org.opencv.core.Size; + +import javax.swing.*; +import java.awt.*; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.*; + +public class InputSourceManager { + + private final EOCVSim eocvSim; + + public volatile Mat lastMatFromSource = null; + public volatile InputSource currentInputSource = null; + + public volatile HashMap sources = new HashMap<>(); + + public InputSourceLoader inputSourceLoader = new InputSourceLoader(); + public SourceSelectorPanel selectorPanel; + + public InputSourceManager(EOCVSim eocvSim) { + this.eocvSim = eocvSim; + selectorPanel = eocvSim.visualizer.sourceSelectorPanel; + } + + public void init() { + + Log.info("InputSourceManager", "Initializing..."); + + if(lastMatFromSource == null) + lastMatFromSource = new Mat(); + + Size size = new Size(320, 240); + createDefaultImgInputSource("/images/ug_4.jpg", "ug_eocvsim_4.jpg", "Ultimate Goal 4 Ring", size); + createDefaultImgInputSource("/images/ug_1.jpg", "ug_eocvsim_1.jpg", "Ultimate Goal 1 Ring", size); + createDefaultImgInputSource("/images/ug_0.jpg", "ug_eocvsim_0.jpg", "Ultimate Goal 0 Ring", size); + + setInputSource("Ultimate Goal 4 Ring"); + + inputSourceLoader.loadInputSourcesFromFile(); + + for (Map.Entry entry : inputSourceLoader.loadedInputSources.entrySet()) { + addInputSource(entry.getKey(), entry.getValue()); + } + + Log.blank(); + } + + private void createDefaultImgInputSource(String resourcePath, String fileName, String sourceName, Size imgSize) { + try { + + InputStream is = InputSource.class.getResourceAsStream(resourcePath); + File f = SysUtil.copyFileIsTemp(is, fileName, true).file; + + ImageSource src = new ImageSource(f.getAbsolutePath(), imgSize); + src.isDefault = true; + src.createdOn = sources.size(); + + addInputSource(sourceName, src); + + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void update(boolean isPaused) { + if(currentInputSource == null) return; + currentInputSource.setPaused(isPaused); + + try { + Mat m = currentInputSource.update(); + if(m != null && !m.empty()) m.copyTo(lastMatFromSource); + } catch(Exception ex) { + Log.error("InputSourceManager", "Error while processing current source", ex); + } + } + + + public void addInputSource(String name, InputSource inputSource) { + addInputSource(name, inputSource, false); + } + + public void addInputSource(String name, InputSource inputSource, boolean dispatchedByUser) { + if (inputSource == null) { + return; + } + + if (sources.containsKey(name)) return; + + if(eocvSim.visualizer.sourceSelectorPanel != null) { + eocvSim.visualizer.sourceSelectorPanel.setAllowSourceSwitching(false); + } + inputSource.name = name; + + sources.put(name, inputSource); + + if(inputSource.createdOn == -1) + inputSource.createdOn = System.currentTimeMillis(); + + if(!inputSource.isDefault) { + inputSourceLoader.saveInputSource(name, inputSource); + inputSourceLoader.saveInputSourcesToFile(); + } + + if(eocvSim.visualizer.sourceSelectorPanel != null) { + SourceSelectorPanel selectorPanel = eocvSim.visualizer.sourceSelectorPanel; + + selectorPanel.updateSourcesList(); + + SwingUtilities.invokeLater(() -> { + JList sourceSelector = selectorPanel.getSourceSelector(); + + int currentSourceIndex = sourceSelector.getSelectedIndex(); + + if(dispatchedByUser) { + int index = selectorPanel.getIndexOf(name); + + sourceSelector.setSelectedIndex(index); + + requestSetInputSource(name); + + eocvSim.onMainUpdate.doOnce(() -> { + eocvSim.pipelineManager.requestSetPaused(false); + pauseIfImageTwoFrames(); + }); + } else { + sourceSelector.setSelectedIndex(currentSourceIndex); + } + + selectorPanel.setAllowSourceSwitching(true); + }); + } + + Log.info("InputSourceManager", "Adding InputSource " + inputSource.toString() + " (" + inputSource.getClass().getSimpleName() + ")"); + } + + public void deleteInputSource(String sourceName) { + InputSource src = sources.get(sourceName); + + if (src == null) return; + if (src.isDefault) return; + + sources.remove(sourceName); + + inputSourceLoader.deleteInputSource(sourceName); + inputSourceLoader.saveInputSourcesToFile(); + } + + public boolean setInputSource(String sourceName) { + InputSource src = sources.get(sourceName); + + if (src != null) { + src.reset(); + src.eocvSim = eocvSim; + } + + //check if source type is a camera, and if so, create a please wait dialog + Visualizer.AsyncPleaseWaitDialog apwd = showApwdIfNeeded(sourceName); + + if (src != null) { + if (!src.init()) { + if (apwd != null) { + apwd.destroyDialog(); + } + + eocvSim.visualizer.asyncPleaseWaitDialog("Error while loading requested source", "Falling back to previous source", + "Close", new Dimension(300, 150), true, true); + + Log.error("InputSourceManager", "Error while loading requested source (" + sourceName + ") reported by itself (init method returned false)"); + + return false; + } + } + + //if there's a please wait dialog for a camera source, destroy it. + if (apwd != null) { + apwd.destroyDialog(); + } + + if (currentInputSource != null) { + currentInputSource.reset(); + } + + currentInputSource = src; + + //if pause on images option is turned on by user + if (eocvSim.configManager.getConfig().pauseOnImages) + pauseIfImage(); + + Log.info("InputSourceManager", "Set InputSource to " + currentInputSource.toString() + " (" + src.getClass().getSimpleName() + ")"); + + return true; + + } + + public boolean isNameOnUse(String name) { + return sources.containsKey(name); + } + + public void pauseIfImage() { + //if the new input source is an image, we will pause the next frame + //to execute one shot analysis on images and save resources. + if (SourceType.fromClass(currentInputSource.getClass()) == SourceType.IMAGE) { + eocvSim.onMainUpdate.doOnce(() -> + eocvSim.pipelineManager.setPaused( + true, + PipelineManager.PauseReason.IMAGE_ONE_ANALYSIS + ) + ); + } + } + + public void pauseIfImageTwoFrames() { + //if the new input source is an image, we will pause the next frame + //to execute one shot analysis on images and save resources. + eocvSim.onMainUpdate.doOnce(this::pauseIfImage); + } + + public void requestSetInputSource(String name) { + eocvSim.onMainUpdate.doOnce(() -> setInputSource(name)); + } + + public Visualizer.AsyncPleaseWaitDialog showApwdIfNeeded(String sourceName) { + Visualizer.AsyncPleaseWaitDialog apwd = null; + + if (getSourceType(sourceName) == SourceType.CAMERA || getSourceType(sourceName) == SourceType.VIDEO) { + apwd = eocvSim.visualizer.asyncPleaseWaitDialog( + "Opening source...", null, "Exit", + new Dimension(300, 150), true + ); + + apwd.onCancel(eocvSim::destroy); + } + + return apwd; + } + + public SourceType getSourceType(String sourceName) { + if(sourceName == null) { + return SourceType.UNKNOWN; + } + + InputSource source = sources.get(sourceName); + + if(source == null) { + return SourceType.UNKNOWN; + } + return SourceType.fromClass(source.getClass()); + } + + public InputSource[] getSortedInputSources() { + ArrayList sources = new ArrayList<>(this.sources.values()); + Collections.sort(sources); + + return sources.toArray(new InputSource[0]); + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/SourceType.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/SourceType.java index edd1ccad..e64178ec 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/SourceType.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/SourceType.java @@ -1,81 +1,81 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.input; - -import com.github.serivesmejia.eocvsim.input.source.*; - -import javax.swing.filechooser.FileFilter; -import java.io.File; - -public enum SourceType { - - IMAGE(new ImageSource(""), "Image"), - CAMERA(new CameraSource(0, null), "Camera"), - VIDEO(new VideoSource("", null), "Video"), - UNKNOWN(null, "Unknown"); - - public final Class klazz; - public final String coolName; - public final InputSource stubInstance; - - SourceType(InputSource instance, String coolName) { - stubInstance = instance; - - if(instance != null) - this.klazz = instance.getClass(); - else - this.klazz = null; - - this.coolName = coolName; - } - - public static SourceType fromClass(Class clazz) { - for(SourceType sourceType : values()) { - if(sourceType.klazz == clazz) { - return sourceType; - } - } - return UNKNOWN; - } - - public static SourceType fromCoolName(String coolName) { - for(SourceType sourceType : values()) { - if(sourceType.coolName.equalsIgnoreCase(coolName)) { - return sourceType; - } - } - return UNKNOWN; - } - - public static SourceType isFileUsableForSource(File file) { - for(SourceType type : values()) { - if(type.stubInstance != null && type.stubInstance.getFileFilters() != null) - if(type.stubInstance.getFileFilters().accept(file)) - return type; - } - - return UNKNOWN; - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.input; + +import com.github.serivesmejia.eocvsim.input.source.*; + +import javax.swing.filechooser.FileFilter; +import java.io.File; + +public enum SourceType { + + IMAGE(new ImageSource(""), "Image"), + CAMERA(new CameraSource(0, null), "Camera"), + VIDEO(new VideoSource("", null), "Video"), + UNKNOWN(null, "Unknown"); + + public final Class klazz; + public final String coolName; + public final InputSource stubInstance; + + SourceType(InputSource instance, String coolName) { + stubInstance = instance; + + if(instance != null) + this.klazz = instance.getClass(); + else + this.klazz = null; + + this.coolName = coolName; + } + + public static SourceType fromClass(Class clazz) { + for(SourceType sourceType : values()) { + if(sourceType.klazz == clazz) { + return sourceType; + } + } + return UNKNOWN; + } + + public static SourceType fromCoolName(String coolName) { + for(SourceType sourceType : values()) { + if(sourceType.coolName.equalsIgnoreCase(coolName)) { + return sourceType; + } + } + return UNKNOWN; + } + + public static SourceType isFileUsableForSource(File file) { + for(SourceType type : values()) { + if(type.stubInstance != null && type.stubInstance.getFileFilters() != null) + if(type.stubInstance.getFileFilters().accept(file)) + return type; + } + + return UNKNOWN; + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.java index cf50f472..8d098f45 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/CameraSource.java @@ -1,202 +1,202 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.input.source; - -import com.github.serivesmejia.eocvsim.gui.Visualizer; -import com.github.serivesmejia.eocvsim.input.InputSource; -import com.github.serivesmejia.eocvsim.util.Log; -import com.google.gson.annotations.Expose; -import org.opencv.core.Mat; -import org.opencv.core.Size; -import org.opencv.imgproc.Imgproc; -import org.opencv.videoio.VideoCapture; -import org.opencv.videoio.Videoio; -import org.openftc.easyopencv.MatRecycler; - -import javax.swing.filechooser.FileFilter; - -public class CameraSource extends InputSource { - - @Expose - private final int webcamIndex; - private transient VideoCapture camera = null; - - private transient MatRecycler.RecyclableMat lastFramePaused = null; - private transient MatRecycler.RecyclableMat lastFrame = null; - - private transient boolean initialized = false; - - @Expose - private volatile Size size; - - private volatile transient MatRecycler matRecycler; - - private transient long capTimeNanos = 0; - - public CameraSource(int webcamIndex, Size size) { - this.webcamIndex = webcamIndex; - this.size = size; - } - - @Override - public boolean init() { - - if (initialized) return false; - initialized = true; - - camera = new VideoCapture(); - camera.open(webcamIndex); - - if (!camera.isOpened()) { - Log.error("CameraSource", "Unable to open camera " + webcamIndex); - return false; - } - - if (matRecycler == null) matRecycler = new MatRecycler(4); - - MatRecycler.RecyclableMat newFrame = matRecycler.takeMat(); - - camera.read(newFrame); - - if (newFrame.empty()) { - Log.error("CameraSource", "Unable to open camera " + webcamIndex + ", returned Mat was empty."); - newFrame.release(); - return false; - } - - matRecycler.returnMat(newFrame); - - return true; - - } - - @Override - public void reset() { - - if (!initialized) return; - if (camera != null && camera.isOpened()) camera.release(); - - if(lastFrame != null && lastFrame.isCheckedOut()) - lastFrame.returnMat(); - if(lastFramePaused != null && lastFramePaused.isCheckedOut()) - lastFramePaused.returnMat(); - - camera = null; - initialized = false; - - } - - @Override - public void close() { - if (camera != null && camera.isOpened()) camera.release(); - } - - @Override - public Mat update() { - - if (isPaused) { - return lastFramePaused; - } else if (lastFramePaused != null) { - lastFramePaused.release(); - lastFramePaused.returnMat(); - lastFramePaused = null; - } - - if (lastFrame == null) lastFrame = matRecycler.takeMat(); - if (camera == null) return lastFrame; - - MatRecycler.RecyclableMat newFrame = matRecycler.takeMat(); - - camera.read(newFrame); - capTimeNanos = System.nanoTime(); - - if (newFrame.empty()) { - newFrame.returnMat(); - return lastFrame; - } - - if (size == null) size = lastFrame.size(); - - Imgproc.cvtColor(newFrame, lastFrame, Imgproc.COLOR_BGR2RGB); - Imgproc.resize(lastFrame, lastFrame, size, 0.0, 0.0, Imgproc.INTER_AREA); - - newFrame.release(); - newFrame.returnMat(); - - return lastFrame; - - } - - @Override - public void onPause() { - - if (lastFrame != null) lastFrame.release(); - if (lastFramePaused == null) lastFramePaused = matRecycler.takeMat(); - - camera.read(lastFramePaused); - - Imgproc.cvtColor(lastFramePaused, lastFramePaused, Imgproc.COLOR_BGR2RGB); - Imgproc.resize(lastFramePaused, lastFramePaused, size, 0.0, 0.0, Imgproc.INTER_AREA); - - update(); - - camera.release(); - camera = null; - - } - - @Override - public void onResume() { - - Visualizer.AsyncPleaseWaitDialog apwdCam = eocvSim.inputSourceManager.showApwdIfNeeded(name); - - camera = new VideoCapture(); - camera.open(webcamIndex); - - apwdCam.destroyDialog(); - - } - - @Override - protected InputSource internalCloneSource() { - return new CameraSource(webcamIndex, size); - } - - @Override - public FileFilter getFileFilters() { - return null; - } - - @Override - public long getCaptureTimeNanos() { - return capTimeNanos; - } - - @Override - public String toString() { - if (size == null) size = new Size(); - return "CameraSource(" + webcamIndex + ", " + (size != null ? size.toString() : "null") + ")"; - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.input.source; + +import com.github.serivesmejia.eocvsim.gui.Visualizer; +import com.github.serivesmejia.eocvsim.input.InputSource; +import com.github.serivesmejia.eocvsim.util.Log; +import com.google.gson.annotations.Expose; +import org.opencv.core.Mat; +import org.opencv.core.Size; +import org.opencv.imgproc.Imgproc; +import org.opencv.videoio.VideoCapture; +import org.opencv.videoio.Videoio; +import org.openftc.easyopencv.MatRecycler; + +import javax.swing.filechooser.FileFilter; + +public class CameraSource extends InputSource { + + @Expose + private final int webcamIndex; + private transient VideoCapture camera = null; + + private transient MatRecycler.RecyclableMat lastFramePaused = null; + private transient MatRecycler.RecyclableMat lastFrame = null; + + private transient boolean initialized = false; + + @Expose + private volatile Size size; + + private volatile transient MatRecycler matRecycler; + + private transient long capTimeNanos = 0; + + public CameraSource(int webcamIndex, Size size) { + this.webcamIndex = webcamIndex; + this.size = size; + } + + @Override + public boolean init() { + + if (initialized) return false; + initialized = true; + + camera = new VideoCapture(); + camera.open(webcamIndex); + + if (!camera.isOpened()) { + Log.error("CameraSource", "Unable to open camera " + webcamIndex); + return false; + } + + if (matRecycler == null) matRecycler = new MatRecycler(4); + + MatRecycler.RecyclableMat newFrame = matRecycler.takeMat(); + + camera.read(newFrame); + + if (newFrame.empty()) { + Log.error("CameraSource", "Unable to open camera " + webcamIndex + ", returned Mat was empty."); + newFrame.release(); + return false; + } + + matRecycler.returnMat(newFrame); + + return true; + + } + + @Override + public void reset() { + + if (!initialized) return; + if (camera != null && camera.isOpened()) camera.release(); + + if(lastFrame != null && lastFrame.isCheckedOut()) + lastFrame.returnMat(); + if(lastFramePaused != null && lastFramePaused.isCheckedOut()) + lastFramePaused.returnMat(); + + camera = null; + initialized = false; + + } + + @Override + public void close() { + if (camera != null && camera.isOpened()) camera.release(); + } + + @Override + public Mat update() { + + if (isPaused) { + return lastFramePaused; + } else if (lastFramePaused != null) { + lastFramePaused.release(); + lastFramePaused.returnMat(); + lastFramePaused = null; + } + + if (lastFrame == null) lastFrame = matRecycler.takeMat(); + if (camera == null) return lastFrame; + + MatRecycler.RecyclableMat newFrame = matRecycler.takeMat(); + + camera.read(newFrame); + capTimeNanos = System.nanoTime(); + + if (newFrame.empty()) { + newFrame.returnMat(); + return lastFrame; + } + + if (size == null) size = lastFrame.size(); + + Imgproc.cvtColor(newFrame, lastFrame, Imgproc.COLOR_BGR2RGB); + Imgproc.resize(lastFrame, lastFrame, size, 0.0, 0.0, Imgproc.INTER_AREA); + + newFrame.release(); + newFrame.returnMat(); + + return lastFrame; + + } + + @Override + public void onPause() { + + if (lastFrame != null) lastFrame.release(); + if (lastFramePaused == null) lastFramePaused = matRecycler.takeMat(); + + camera.read(lastFramePaused); + + Imgproc.cvtColor(lastFramePaused, lastFramePaused, Imgproc.COLOR_BGR2RGB); + Imgproc.resize(lastFramePaused, lastFramePaused, size, 0.0, 0.0, Imgproc.INTER_AREA); + + update(); + + camera.release(); + camera = null; + + } + + @Override + public void onResume() { + + Visualizer.AsyncPleaseWaitDialog apwdCam = eocvSim.inputSourceManager.showApwdIfNeeded(name); + + camera = new VideoCapture(); + camera.open(webcamIndex); + + apwdCam.destroyDialog(); + + } + + @Override + protected InputSource internalCloneSource() { + return new CameraSource(webcamIndex, size); + } + + @Override + public FileFilter getFileFilters() { + return null; + } + + @Override + public long getCaptureTimeNanos() { + return capTimeNanos; + } + + @Override + public String toString() { + if (size == null) size = new Size(); + return "CameraSource(" + webcamIndex + ", " + (size != null ? size.toString() : "null") + ")"; + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java index 7d605980..39187cd6 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/ImageSource.java @@ -1,178 +1,178 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.input.source; - -import com.github.serivesmejia.eocvsim.input.InputSource; -import com.github.serivesmejia.eocvsim.util.FileFilters; -import com.google.gson.annotations.Expose; -import org.opencv.core.Mat; -import org.opencv.core.Size; -import org.opencv.imgcodecs.Imgcodecs; -import org.opencv.imgproc.Imgproc; -import org.openftc.easyopencv.MatRecycler; - -import javax.swing.filechooser.FileFilter; - -public class ImageSource extends InputSource { - - @Expose - private final String imgPath; - @Expose - private volatile Size size; - - private volatile transient MatRecycler.RecyclableMat img; - private volatile transient MatRecycler.RecyclableMat lastCloneTo; - - private volatile transient boolean initialized = false; - - private volatile transient MatRecycler matRecycler = new MatRecycler(2); - - public ImageSource(String imgPath) { - this(imgPath, null); - } - - public ImageSource(String imgPath, Size size) { - this.imgPath = imgPath; - this.size = size; - } - - @Override - public boolean init() { - - if (initialized) return false; - initialized = true; - - if (matRecycler == null) matRecycler = new MatRecycler(2); - - readImage(); - - return img != null && !img.empty(); - - } - - @Override - public void onPause() { - //if(img != null) img.release(); - } - - @Override - public void onResume() { - } - - @Override - public void reset() { - - if (!initialized) return; - - if (lastCloneTo != null) { - lastCloneTo.returnMat(); - lastCloneTo = null; - } - - if (img != null) { - img.returnMat(); - img = null; - } - - matRecycler.releaseAll(); - - initialized = false; - - } - - public void close() { - - if (img != null) { - matRecycler.returnMat(img); - img = null; - } - - if (lastCloneTo != null) { - lastCloneTo.returnMat(); - lastCloneTo = null; - } - - matRecycler.releaseAll(); - - } - - public void readImage() { - - Mat readMat = Imgcodecs.imread(this.imgPath); - - if (img == null) img = matRecycler.takeMat(); - - if (readMat.empty()) { - return; - } - - readMat.copyTo(img); - readMat.release(); - - if (this.size != null) { - Imgproc.resize(img, img, this.size, 0.0, 0.0, Imgproc.INTER_AREA); - } else { - this.size = img.size(); - } - - Imgproc.cvtColor(img, img, Imgproc.COLOR_BGR2RGB); - - } - - @Override - public Mat update() { - - if (isPaused) return lastCloneTo; - if (lastCloneTo == null) lastCloneTo = matRecycler.takeMat(); - - if (img == null) return null; - - img.copyTo(lastCloneTo); - - return lastCloneTo; - - } - - @Override - protected InputSource internalCloneSource() { - return new ImageSource(imgPath, size); - } - - @Override - public FileFilter getFileFilters() { - return FileFilters.imagesFilter; - } - - @Override - public long getCaptureTimeNanos() { - return System.nanoTime(); - } - - @Override - public String toString() { - if (size == null) size = new Size(); - return "ImageSource(\"" + imgPath + "\", " + (size != null ? size.toString() : "null") + ")"; - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.input.source; + +import com.github.serivesmejia.eocvsim.input.InputSource; +import com.github.serivesmejia.eocvsim.util.FileFilters; +import com.google.gson.annotations.Expose; +import org.opencv.core.Mat; +import org.opencv.core.Size; +import org.opencv.imgcodecs.Imgcodecs; +import org.opencv.imgproc.Imgproc; +import org.openftc.easyopencv.MatRecycler; + +import javax.swing.filechooser.FileFilter; + +public class ImageSource extends InputSource { + + @Expose + private final String imgPath; + @Expose + private volatile Size size; + + private volatile transient MatRecycler.RecyclableMat img; + private volatile transient MatRecycler.RecyclableMat lastCloneTo; + + private volatile transient boolean initialized = false; + + private volatile transient MatRecycler matRecycler = new MatRecycler(2); + + public ImageSource(String imgPath) { + this(imgPath, null); + } + + public ImageSource(String imgPath, Size size) { + this.imgPath = imgPath; + this.size = size; + } + + @Override + public boolean init() { + + if (initialized) return false; + initialized = true; + + if (matRecycler == null) matRecycler = new MatRecycler(2); + + readImage(); + + return img != null && !img.empty(); + + } + + @Override + public void onPause() { + //if(img != null) img.release(); + } + + @Override + public void onResume() { + } + + @Override + public void reset() { + + if (!initialized) return; + + if (lastCloneTo != null) { + lastCloneTo.returnMat(); + lastCloneTo = null; + } + + if (img != null) { + img.returnMat(); + img = null; + } + + matRecycler.releaseAll(); + + initialized = false; + + } + + public void close() { + + if (img != null) { + matRecycler.returnMat(img); + img = null; + } + + if (lastCloneTo != null) { + lastCloneTo.returnMat(); + lastCloneTo = null; + } + + matRecycler.releaseAll(); + + } + + public void readImage() { + + Mat readMat = Imgcodecs.imread(this.imgPath); + + if (img == null) img = matRecycler.takeMat(); + + if (readMat.empty()) { + return; + } + + readMat.copyTo(img); + readMat.release(); + + if (this.size != null) { + Imgproc.resize(img, img, this.size, 0.0, 0.0, Imgproc.INTER_AREA); + } else { + this.size = img.size(); + } + + Imgproc.cvtColor(img, img, Imgproc.COLOR_BGR2RGB); + + } + + @Override + public Mat update() { + + if (isPaused) return lastCloneTo; + if (lastCloneTo == null) lastCloneTo = matRecycler.takeMat(); + + if (img == null) return null; + + img.copyTo(lastCloneTo); + + return lastCloneTo; + + } + + @Override + protected InputSource internalCloneSource() { + return new ImageSource(imgPath, size); + } + + @Override + public FileFilter getFileFilters() { + return FileFilters.imagesFilter; + } + + @Override + public long getCaptureTimeNanos() { + return System.nanoTime(); + } + + @Override + public String toString() { + if (size == null) size = new Size(); + return "ImageSource(\"" + imgPath + "\", " + (size != null ? size.toString() : "null") + ")"; + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java index 35c70c7d..3c11120b 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/source/VideoSource.java @@ -1,218 +1,218 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.input.source; - -import com.github.serivesmejia.eocvsim.gui.Visualizer; -import com.github.serivesmejia.eocvsim.input.InputSource; -import com.github.serivesmejia.eocvsim.util.FileFilters; -import com.github.serivesmejia.eocvsim.util.Log; -import com.google.gson.annotations.Expose; -import org.opencv.core.Mat; -import org.opencv.core.Size; -import org.opencv.imgproc.Imgproc; -import org.opencv.videoio.VideoCapture; -import org.opencv.videoio.Videoio; -import org.openftc.easyopencv.MatRecycler; - -import javax.swing.filechooser.FileFilter; -import java.util.Objects; - -public class VideoSource extends InputSource { - - @Expose - private final String videoPath; - - private transient VideoCapture video = null; - - private transient MatRecycler.RecyclableMat lastFramePaused = null; - private transient MatRecycler.RecyclableMat lastFrame = null; - - private transient boolean initialized = false; - - @Expose - private volatile Size size; - - private volatile transient MatRecycler matRecycler = null; - - private transient double lastFramePosition = 0; - - private transient long capTimeNanos = 0; - - public VideoSource(String videoPath, Size size) { - this.videoPath = videoPath; - this.size = size; - } - - @Override - public boolean init() { - - if (initialized) return false; - initialized = true; - - video = new VideoCapture(); - video.open(videoPath); - - if (!video.isOpened()) { - Log.error("VideoSource", "Unable to open video " + videoPath); - return false; - } - - if (matRecycler == null) matRecycler = new MatRecycler(4); - - MatRecycler.RecyclableMat newFrame = matRecycler.takeMat(); - newFrame.release(); - - video.read(newFrame); - - if (newFrame.empty()) { - Log.error("VideoSource", "Unable to open video " + videoPath + ", returned Mat was empty."); - return false; - } - - newFrame.release(); - matRecycler.returnMat(newFrame); - - return true; - - } - - @Override - public void reset() { - - if (!initialized) return; - - if (video != null && video.isOpened()) video.release(); - - if(lastFrame != null && lastFrame.isCheckedOut()) - lastFrame.returnMat(); - if(lastFramePaused != null && lastFramePaused.isCheckedOut()) - lastFramePaused.returnMat(); - - matRecycler.releaseAll(); - - video = null; - initialized = false; - - } - - @Override - public void close() { - - if(video != null && video.isOpened()) video.release(); - if(lastFrame != null) lastFrame.returnMat(); - - if (lastFramePaused != null) { - lastFramePaused.returnMat(); - lastFramePaused = null; - } - - } - - @Override - public Mat update() { - - if (isPaused) { - return lastFramePaused; - } else if (lastFramePaused != null) { - lastFramePaused.returnMat(); - lastFramePaused = null; - } - - if (lastFrame == null) lastFrame = matRecycler.takeMat(); - if (video == null) return lastFrame; - - MatRecycler.RecyclableMat newFrame = matRecycler.takeMat(); - - video.read(newFrame); - capTimeNanos = System.nanoTime(); - - //with videocapture for video files, when an empty mat is returned - //the most likely reason is that the video ended, so we set the - //playback position back to 0 for looping in here and start over - //in next update - if (newFrame.empty()) { - newFrame.returnMat(); - video.set(Videoio.CAP_PROP_POS_FRAMES, 0); - return lastFrame; - } - - if (size == null) size = lastFrame.size(); - - Imgproc.cvtColor(newFrame, lastFrame, Imgproc.COLOR_BGR2RGB); - Imgproc.resize(lastFrame, lastFrame, size, 0.0, 0.0, Imgproc.INTER_AREA); - - matRecycler.returnMat(newFrame); - - return lastFrame; - - } - - @Override - public void onPause() { - - if (lastFrame != null) lastFrame.release(); - if (lastFramePaused == null) lastFramePaused = matRecycler.takeMat(); - - video.read(lastFramePaused); - - Imgproc.cvtColor(lastFramePaused, lastFramePaused, Imgproc.COLOR_BGR2RGB); - Imgproc.resize(lastFramePaused, lastFramePaused, size, 0.0, 0.0, Imgproc.INTER_AREA); - - update(); - - lastFramePosition = video.get(Videoio.CAP_PROP_POS_FRAMES); - - video.release(); - video = null; - - } - - @Override - public void onResume() { - video = new VideoCapture(); - video.open(videoPath); - video.set(Videoio.CAP_PROP_POS_FRAMES, lastFramePosition); - } - - @Override - protected InputSource internalCloneSource() { - return new VideoSource(videoPath, size); - } - - @Override - public FileFilter getFileFilters() { - return FileFilters.videoMediaFilter; - } - - @Override - public long getCaptureTimeNanos() { - return capTimeNanos; - } - - @Override - public String toString() { - return "VideoSource(" + videoPath + ", " + (size != null ? size.toString() : "null") + ")"; - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.input.source; + +import com.github.serivesmejia.eocvsim.gui.Visualizer; +import com.github.serivesmejia.eocvsim.input.InputSource; +import com.github.serivesmejia.eocvsim.util.FileFilters; +import com.github.serivesmejia.eocvsim.util.Log; +import com.google.gson.annotations.Expose; +import org.opencv.core.Mat; +import org.opencv.core.Size; +import org.opencv.imgproc.Imgproc; +import org.opencv.videoio.VideoCapture; +import org.opencv.videoio.Videoio; +import org.openftc.easyopencv.MatRecycler; + +import javax.swing.filechooser.FileFilter; +import java.util.Objects; + +public class VideoSource extends InputSource { + + @Expose + private final String videoPath; + + private transient VideoCapture video = null; + + private transient MatRecycler.RecyclableMat lastFramePaused = null; + private transient MatRecycler.RecyclableMat lastFrame = null; + + private transient boolean initialized = false; + + @Expose + private volatile Size size; + + private volatile transient MatRecycler matRecycler = null; + + private transient double lastFramePosition = 0; + + private transient long capTimeNanos = 0; + + public VideoSource(String videoPath, Size size) { + this.videoPath = videoPath; + this.size = size; + } + + @Override + public boolean init() { + + if (initialized) return false; + initialized = true; + + video = new VideoCapture(); + video.open(videoPath); + + if (!video.isOpened()) { + Log.error("VideoSource", "Unable to open video " + videoPath); + return false; + } + + if (matRecycler == null) matRecycler = new MatRecycler(4); + + MatRecycler.RecyclableMat newFrame = matRecycler.takeMat(); + newFrame.release(); + + video.read(newFrame); + + if (newFrame.empty()) { + Log.error("VideoSource", "Unable to open video " + videoPath + ", returned Mat was empty."); + return false; + } + + newFrame.release(); + matRecycler.returnMat(newFrame); + + return true; + + } + + @Override + public void reset() { + + if (!initialized) return; + + if (video != null && video.isOpened()) video.release(); + + if(lastFrame != null && lastFrame.isCheckedOut()) + lastFrame.returnMat(); + if(lastFramePaused != null && lastFramePaused.isCheckedOut()) + lastFramePaused.returnMat(); + + matRecycler.releaseAll(); + + video = null; + initialized = false; + + } + + @Override + public void close() { + + if(video != null && video.isOpened()) video.release(); + if(lastFrame != null) lastFrame.returnMat(); + + if (lastFramePaused != null) { + lastFramePaused.returnMat(); + lastFramePaused = null; + } + + } + + @Override + public Mat update() { + + if (isPaused) { + return lastFramePaused; + } else if (lastFramePaused != null) { + lastFramePaused.returnMat(); + lastFramePaused = null; + } + + if (lastFrame == null) lastFrame = matRecycler.takeMat(); + if (video == null) return lastFrame; + + MatRecycler.RecyclableMat newFrame = matRecycler.takeMat(); + + video.read(newFrame); + capTimeNanos = System.nanoTime(); + + //with videocapture for video files, when an empty mat is returned + //the most likely reason is that the video ended, so we set the + //playback position back to 0 for looping in here and start over + //in next update + if (newFrame.empty()) { + newFrame.returnMat(); + video.set(Videoio.CAP_PROP_POS_FRAMES, 0); + return lastFrame; + } + + if (size == null) size = lastFrame.size(); + + Imgproc.cvtColor(newFrame, lastFrame, Imgproc.COLOR_BGR2RGB); + Imgproc.resize(lastFrame, lastFrame, size, 0.0, 0.0, Imgproc.INTER_AREA); + + matRecycler.returnMat(newFrame); + + return lastFrame; + + } + + @Override + public void onPause() { + + if (lastFrame != null) lastFrame.release(); + if (lastFramePaused == null) lastFramePaused = matRecycler.takeMat(); + + video.read(lastFramePaused); + + Imgproc.cvtColor(lastFramePaused, lastFramePaused, Imgproc.COLOR_BGR2RGB); + Imgproc.resize(lastFramePaused, lastFramePaused, size, 0.0, 0.0, Imgproc.INTER_AREA); + + update(); + + lastFramePosition = video.get(Videoio.CAP_PROP_POS_FRAMES); + + video.release(); + video = null; + + } + + @Override + public void onResume() { + video = new VideoCapture(); + video.open(videoPath); + video.set(Videoio.CAP_PROP_POS_FRAMES, lastFramePosition); + } + + @Override + protected InputSource internalCloneSource() { + return new VideoSource(videoPath, size); + } + + @Override + public FileFilter getFileFilters() { + return FileFilters.videoMediaFilter; + } + + @Override + public long getCaptureTimeNanos() { + return capTimeNanos; + } + + @Override + public String toString() { + return "VideoSource(" + videoPath + ", " + (size != null ? size.toString() : "null") + ")"; + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/output/VideoRecordingSession.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/output/VideoRecordingSession.kt index 2b69495f..81e0d6d9 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/output/VideoRecordingSession.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/output/VideoRecordingSession.kt @@ -1,160 +1,160 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.output - -import com.github.serivesmejia.eocvsim.gui.util.MatPoster -import com.github.serivesmejia.eocvsim.util.StrUtil -import com.github.serivesmejia.eocvsim.util.extension.aspectRatio -import com.github.serivesmejia.eocvsim.util.extension.clipTo -import com.github.serivesmejia.eocvsim.util.fps.FpsCounter -import org.opencv.core.* -import org.opencv.imgproc.Imgproc -import org.opencv.videoio.VideoWriter -import java.io.File -import java.nio.file.Files -import java.nio.file.StandardCopyOption -import kotlin.math.roundToInt - -class VideoRecordingSession( - val videoFps: Double = 30.0, - val videoSize: Size = Size(320.0, 240.0), - val isFramesRgb: Boolean = true -) { - - private val videoWriter = VideoWriter() - private val tempFile = File.createTempFile(StrUtil.random(), ".avi") - - @Volatile private var videoMat: Mat? = null - - val matPoster = MatPoster("VideoRec", videoFps.toInt()) - - private val fpsCounter = FpsCounter() - - @Volatile var hasStarted = false - private set - @Volatile var hasStopped = false - private set - - val isRecording get() = hasStarted && !hasStopped - - init { - matPoster.addPostable { postMat(it) } - } - - fun startRecordingSession() { - videoWriter.open(tempFile.toString(), VideoWriter.fourcc('M', 'J', 'P', 'G'), videoFps, videoSize) - hasStarted = true; - } - - fun stopRecordingSession() { - videoWriter.release(); videoMat?.release(); matPoster.stop() - hasStopped = true - } - - fun saveTo(file: File) { - if(!hasStopped) return - Files.copy(tempFile.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING) - Files.delete(tempFile.toPath()) - } - - fun discardVideo() { - if(tempFile.exists()) - Files.delete(tempFile.toPath()) - } - - @Synchronized fun postMatAsync(inputMat: Mat) { - if(!videoWriter.isOpened) return - matPoster.post(inputMat) - } - - @Synchronized fun postMat(inputMat: Mat) { - if(!videoWriter.isOpened) return - - if(videoMat == null) - videoMat = Mat(videoSize, inputMat.type()) - else - videoMat!!.setTo(Scalar(0.0, 0.0, 0.0)) - - //we need BGR frames - if(isFramesRgb) { - Imgproc.cvtColor(inputMat, inputMat, Imgproc.COLOR_RGB2BGR) - } - - if(inputMat.size() == videoSize) { //nice, the mat size is the exact same as the video size - compensateFpsWrite(inputMat, fpsCounter.fps.toDouble(), videoFps) - } else { //uh oh, this might get a bit harder here... - val videoR = videoSize.aspectRatio() - val inputR = inputMat.aspectRatio() - - //ok, we have the same aspect ratio, we can just scale to the required size - if(videoR == inputR) { - Imgproc.resize(inputMat, videoMat, videoSize, 0.0, 0.0, Imgproc.INTER_AREA) - compensateFpsWrite(videoMat!!, fpsCounter.fps.toDouble(), videoFps) - } else { //hmm, not the same aspect ratio, we'll need to do some fancy stuff here... - val inputW = inputMat.size().width - val inputH = inputMat.size().height - - val widthRatio = videoSize.width / inputW - val heightRatio = videoSize.height / inputH - val bestRatio = widthRatio.coerceAtMost(heightRatio) - - val newSize = Size(inputW * bestRatio, inputH * bestRatio).clipTo(videoSize) - - //get offsets so that we center the image instead of leaving it at (0,0) - //(basically the black bars you see) - val xOffset = (videoSize.width - newSize.width) / 2 - val yOffset = (videoSize.height - newSize.height) / 2 - - Imgproc.resize(inputMat, inputMat, newSize, 0.0, 0.0, Imgproc.INTER_AREA) - - //get submat of the exact required size and offset position from the "videoMat", - //which has the user-defined size of the current video. - val submat = videoMat!!.submat(Rect(Point(xOffset, yOffset), newSize)) - - //then we copy our adjusted mat into the gotten submat. since a submat is just - //a reference to the parent mat, when we copy here our data will be actually - //copied to the actual mat, and so our new mat will be of the correct size and - //centered with the required offset - inputMat.copyTo(submat); - - compensateFpsWrite(videoMat!!, fpsCounter.fps.toDouble(), videoFps) - } - - fpsCounter.update() - } - } - - //compensating for variable fps, we write the same mat multiple - //times so that our video stays in sync with the correct speed. - @Synchronized private fun compensateFpsWrite(mat: Mat, currentFps: Double, targetFps: Double) { - if (currentFps < targetFps && currentFps > 0) { - repeat((targetFps / currentFps).roundToInt()) { - videoWriter.write(mat) - } - } else { - videoWriter.write(mat) - } - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.output + +import com.github.serivesmejia.eocvsim.gui.util.MatPoster +import com.github.serivesmejia.eocvsim.util.StrUtil +import com.github.serivesmejia.eocvsim.util.extension.aspectRatio +import com.github.serivesmejia.eocvsim.util.extension.clipTo +import com.github.serivesmejia.eocvsim.util.fps.FpsCounter +import org.opencv.core.* +import org.opencv.imgproc.Imgproc +import org.opencv.videoio.VideoWriter +import java.io.File +import java.nio.file.Files +import java.nio.file.StandardCopyOption +import kotlin.math.roundToInt + +class VideoRecordingSession( + val videoFps: Double = 30.0, + val videoSize: Size = Size(320.0, 240.0), + val isFramesRgb: Boolean = true +) { + + private val videoWriter = VideoWriter() + private val tempFile = File.createTempFile(StrUtil.random(), ".avi") + + @Volatile private var videoMat: Mat? = null + + val matPoster = MatPoster("VideoRec", videoFps.toInt()) + + private val fpsCounter = FpsCounter() + + @Volatile var hasStarted = false + private set + @Volatile var hasStopped = false + private set + + val isRecording get() = hasStarted && !hasStopped + + init { + matPoster.addPostable { postMat(it) } + } + + fun startRecordingSession() { + videoWriter.open(tempFile.toString(), VideoWriter.fourcc('M', 'J', 'P', 'G'), videoFps, videoSize) + hasStarted = true; + } + + fun stopRecordingSession() { + videoWriter.release(); videoMat?.release(); matPoster.stop() + hasStopped = true + } + + fun saveTo(file: File) { + if(!hasStopped) return + Files.copy(tempFile.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING) + Files.delete(tempFile.toPath()) + } + + fun discardVideo() { + if(tempFile.exists()) + Files.delete(tempFile.toPath()) + } + + @Synchronized fun postMatAsync(inputMat: Mat) { + if(!videoWriter.isOpened) return + matPoster.post(inputMat) + } + + @Synchronized fun postMat(inputMat: Mat) { + if(!videoWriter.isOpened) return + + if(videoMat == null) + videoMat = Mat(videoSize, inputMat.type()) + else + videoMat!!.setTo(Scalar(0.0, 0.0, 0.0)) + + //we need BGR frames + if(isFramesRgb) { + Imgproc.cvtColor(inputMat, inputMat, Imgproc.COLOR_RGB2BGR) + } + + if(inputMat.size() == videoSize) { //nice, the mat size is the exact same as the video size + compensateFpsWrite(inputMat, fpsCounter.fps.toDouble(), videoFps) + } else { //uh oh, this might get a bit harder here... + val videoR = videoSize.aspectRatio() + val inputR = inputMat.aspectRatio() + + //ok, we have the same aspect ratio, we can just scale to the required size + if(videoR == inputR) { + Imgproc.resize(inputMat, videoMat, videoSize, 0.0, 0.0, Imgproc.INTER_AREA) + compensateFpsWrite(videoMat!!, fpsCounter.fps.toDouble(), videoFps) + } else { //hmm, not the same aspect ratio, we'll need to do some fancy stuff here... + val inputW = inputMat.size().width + val inputH = inputMat.size().height + + val widthRatio = videoSize.width / inputW + val heightRatio = videoSize.height / inputH + val bestRatio = widthRatio.coerceAtMost(heightRatio) + + val newSize = Size(inputW * bestRatio, inputH * bestRatio).clipTo(videoSize) + + //get offsets so that we center the image instead of leaving it at (0,0) + //(basically the black bars you see) + val xOffset = (videoSize.width - newSize.width) / 2 + val yOffset = (videoSize.height - newSize.height) / 2 + + Imgproc.resize(inputMat, inputMat, newSize, 0.0, 0.0, Imgproc.INTER_AREA) + + //get submat of the exact required size and offset position from the "videoMat", + //which has the user-defined size of the current video. + val submat = videoMat!!.submat(Rect(Point(xOffset, yOffset), newSize)) + + //then we copy our adjusted mat into the gotten submat. since a submat is just + //a reference to the parent mat, when we copy here our data will be actually + //copied to the actual mat, and so our new mat will be of the correct size and + //centered with the required offset + inputMat.copyTo(submat); + + compensateFpsWrite(videoMat!!, fpsCounter.fps.toDouble(), videoFps) + } + + fpsCounter.update() + } + } + + //compensating for variable fps, we write the same mat multiple + //times so that our video stays in sync with the correct speed. + @Synchronized private fun compensateFpsWrite(mat: Mat, currentFps: Double, targetFps: Double) { + if (currentFps < targetFps && currentFps > 0) { + repeat((targetFps / currentFps).roundToInt()) { + videoWriter.write(mat) + } + } else { + videoWriter.write(mat) + } + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/DefaultPipeline.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/DefaultPipeline.java index 40cc3460..1104f6c0 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/DefaultPipeline.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/DefaultPipeline.java @@ -1,83 +1,83 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.pipeline; - -import org.firstinspires.ftc.robotcore.external.Telemetry; -import org.opencv.core.*; -import org.opencv.imgproc.Imgproc; -import org.openftc.easyopencv.OpenCvPipeline; - -public class DefaultPipeline extends OpenCvPipeline { - - public int blur = 0; - - private Telemetry telemetry; - - public DefaultPipeline(Telemetry telemetry) { - this.telemetry = telemetry; - } - - @Override - public Mat processFrame(Mat input) { - - double aspectRatio = (double) input.height() / (double) input.width(); - double aspectRatioPercentage = aspectRatio / (580.0 / 480.0); - - telemetry.addData("[>]", "Default pipeline selected."); - telemetry.addData("[Aspect Ratio]", aspectRatio + " (" + String.format("%.2f", aspectRatioPercentage * 100) + "%)"); - telemetry.addData("[Blur]", blur + " (change this value in tuner menu)"); - telemetry.update(); - - if (blur > 0 && blur % 2 == 1) { - Imgproc.GaussianBlur(input, input, new Size(blur, blur), 0); - } - - // Outline - Imgproc.putText( - input, - "Default pipeline selected", - new Point(0, 22 * aspectRatioPercentage), - Imgproc.FONT_HERSHEY_PLAIN, - 2 * aspectRatioPercentage, - new Scalar(255, 255, 255), - (int) Math.round(5 * aspectRatioPercentage) - ); - - - //Text - Imgproc.putText( - input, - "Default pipeline selected", - new Point(0, 22 * aspectRatioPercentage), - Imgproc.FONT_HERSHEY_PLAIN, - 2 * aspectRatioPercentage, - new Scalar(0, 0, 0), - (int) Math.round(2 * aspectRatioPercentage) - ); - - return input; - - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.pipeline; + +import org.firstinspires.ftc.robotcore.external.Telemetry; +import org.opencv.core.*; +import org.opencv.imgproc.Imgproc; +import org.openftc.easyopencv.OpenCvPipeline; + +public class DefaultPipeline extends OpenCvPipeline { + + public int blur = 0; + + private Telemetry telemetry; + + public DefaultPipeline(Telemetry telemetry) { + this.telemetry = telemetry; + } + + @Override + public Mat processFrame(Mat input) { + + double aspectRatio = (double) input.height() / (double) input.width(); + double aspectRatioPercentage = aspectRatio / (580.0 / 480.0); + + telemetry.addData("[>]", "Default pipeline selected."); + telemetry.addData("[Aspect Ratio]", aspectRatio + " (" + String.format("%.2f", aspectRatioPercentage * 100) + "%)"); + telemetry.addData("[Blur]", blur + " (change this value in tuner menu)"); + telemetry.update(); + + if (blur > 0 && blur % 2 == 1) { + Imgproc.GaussianBlur(input, input, new Size(blur, blur), 0); + } + + // Outline + Imgproc.putText( + input, + "Default pipeline selected", + new Point(0, 22 * aspectRatioPercentage), + Imgproc.FONT_HERSHEY_PLAIN, + 2 * aspectRatioPercentage, + new Scalar(255, 255, 255), + (int) Math.round(5 * aspectRatioPercentage) + ); + + + //Text + Imgproc.putText( + input, + "Default pipeline selected", + new Point(0, 22 * aspectRatioPercentage), + Imgproc.FONT_HERSHEY_PLAIN, + 2 * aspectRatioPercentage, + new Scalar(0, 0, 0), + (int) Math.round(2 * aspectRatioPercentage) + ); + + return input; + + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt index 65760fce..a97e4062 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt @@ -1,642 +1,642 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.pipeline - -import com.github.serivesmejia.eocvsim.EOCVSim -import com.github.serivesmejia.eocvsim.gui.DialogFactory -import com.github.serivesmejia.eocvsim.gui.util.MatPoster -import com.github.serivesmejia.eocvsim.pipeline.compiler.CompiledPipelineManager -import com.github.serivesmejia.eocvsim.pipeline.util.PipelineExceptionTracker -import com.github.serivesmejia.eocvsim.pipeline.util.PipelineSnapshot -import com.github.serivesmejia.eocvsim.util.Log -import com.github.serivesmejia.eocvsim.util.event.EventHandler -import com.github.serivesmejia.eocvsim.util.exception.MaxActiveContextsException -import com.github.serivesmejia.eocvsim.util.fps.FpsCounter -import kotlinx.coroutines.* -import org.firstinspires.ftc.robotcore.external.Telemetry -import org.opencv.core.Mat -import org.openftc.easyopencv.OpenCvPipeline -import org.openftc.easyopencv.TimestampedPipelineHandler -import java.lang.reflect.Constructor -import java.util.* -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.math.roundToLong - -@OptIn(DelicateCoroutinesApi::class) -class PipelineManager(var eocvSim: EOCVSim) { - - companion object { - const val MAX_ALLOWED_ACTIVE_PIPELINE_CONTEXTS = 5 - - var staticSnapshot: PipelineSnapshot? = null - private set - - private const val TAG = "PipelineManager" - } - - @JvmField val onUpdate = EventHandler("OnPipelineUpdate") - @JvmField val onPipelineChange = EventHandler("OnPipelineChange") - @JvmField val onPipelineTimeout = EventHandler("OnPipelineTimeout") - @JvmField val onPause = EventHandler("OnPipelinePause") - @JvmField val onResume = EventHandler("OnPipelineResume") - - val pipelineOutputPosters = ArrayList() - val pipelineFpsCounter = FpsCounter() - - private var hasInitCurrentPipeline = false - var lastPipelineAction = "processFrame" - private set - - val pipelines = ArrayList() - - @Volatile var currentPipeline: OpenCvPipeline? = null - private set - @Volatile var currentPipelineData: PipelineData? = null - private set - var currentPipelineName = "" - private set - var currentPipelineIndex = -1 - private set - - val activePipelineContexts = ArrayList() - private var currentPipelineContext: ExecutorCoroutineDispatcher? = null - - @Volatile var currentTelemetry: Telemetry? = null - private set - - @Volatile var paused = false - private set - get() { - if (!field) pauseReason = PauseReason.NOT_PAUSED - return field - } - - var pauseReason = PauseReason.NOT_PAUSED - private set - get() { - if (!paused) field = PauseReason.NOT_PAUSED - return field - } - - var latestSnapshot: PipelineSnapshot? = null - private set - - var lastInitialSnapshot: PipelineSnapshot? = null - private set - - //manages and builds pipelines in runtime - @JvmField val compiledPipelineManager = CompiledPipelineManager(this) - //this will be handling the special pipeline "timestamped" type - val timestampedPipelineHandler = TimestampedPipelineHandler() - //counting and tracking exceptions for logging and reporting purposes - val pipelineExceptionTracker = PipelineExceptionTracker(this) - - private var openedPipelineOutputCount = 0 - - enum class PauseReason { - USER_REQUESTED, IMAGE_ONE_ANALYSIS, NOT_PAUSED - } - - fun init() { - Log.info(TAG, "Initializing...") - - //add default pipeline - addPipelineClass(DefaultPipeline::class.java) - - //scan for pipelines - PipelineScanner(eocvSim.params.scanForPipelinesIn).lookForPipelines { - addPipelineClass(it) - } - - Log.info(TAG, "Found " + pipelines.size + " pipeline(s)") - Log.blank() - - compiledPipelineManager.init() - - // changing to initial pipeline - onUpdate.doOnce { - if(compiledPipelineManager.isBuildRunning) - compiledPipelineManager.onBuildEnd.doOnce(::applyStaticSnapOrDef) - else - applyStaticSnapOrDef() - } - - pipelineExceptionTracker.onNewPipelineException { - if(openedPipelineOutputCount <= 3) { - DialogFactory.createPipelineOutput(eocvSim) - openedPipelineOutputCount++ - } - - currentTelemetry?.errItem?.caption = "[/!\\]" - currentTelemetry?.errItem?.setValue("Uncaught exception thrown in\n pipeline, check Workspace -> Output.") - } - - pipelineExceptionTracker.onPipelineExceptionClear { - currentTelemetry?.errItem?.caption = "" - currentTelemetry?.errItem?.setValue("") - } - - onPipelineChange { - openedPipelineOutputCount = 0 - } - } - - private fun applyStaticSnapOrDef() { - onUpdate.doOnce { - if(!applyStaticSnapshot()) { - val params = eocvSim.params - - // changing to the initial pipeline, defined by the eocv sim parameters or the default pipeline - if(params.initialPipelineName != null) { - changePipeline(params.initialPipelineName!!, params.initialPipelineSource ?: PipelineSource.CLASSPATH) - } else { - forceChangePipeline(0) - } - } - - eocvSim.visualizer.pipelineSelectorPanel.allowPipelineSwitching = true - } - } - - fun update(inputMat: Mat?) { - onUpdate.run() - - if(activePipelineContexts.size > MAX_ALLOWED_ACTIVE_PIPELINE_CONTEXTS) { - throw MaxActiveContextsException("Current amount of active pipeline coroutine contexts (${activePipelineContexts.size}) is more than the maximum allowed. This generally means that there are multiple pipelines stuck in processFrame() running in the background, check for any lengthy operations in your pipelines.") - } - - if(compiledPipelineManager.isBuildRunning) { - currentTelemetry?.infoItem?.caption = "[>]" - currentTelemetry?.infoItem?.setValue("Building java files in workspace...") - } else { - currentTelemetry?.infoItem?.caption = "" - currentTelemetry?.infoItem?.setValue("") - } - - if(paused || currentPipeline == null) { - updateExceptionTracker() - return - } - - timestampedPipelineHandler.update(currentPipeline, eocvSim.inputSourceManager.currentInputSource) - - lastPipelineAction = if(!hasInitCurrentPipeline) { - "init/processFrame" - } else { - "processFrame" - } - - //run our pipeline in the background until it finishes or gets cancelled - val pipelineJob = GlobalScope.launch(currentPipelineContext!!) { - try { - //if we have a pipeline, we run it right here, passing the input mat - //given to us. we'll post the frame the pipeline returns as long - //as we haven't ran out of time (the main loop will not wait it - //forever to finish its job). if we run out of time, and if the - //pipeline ever returns, we will not post the frame, since we - //don't know when it was actually requested, we might even be in - //a different pipeline at this point. we also call init if we - //haven't done so. - - if(!hasInitCurrentPipeline && inputMat != null) { - currentPipeline?.init(inputMat) - - Log.info("PipelineManager", "Initialized pipeline $currentPipelineName") - Log.blank() - - hasInitCurrentPipeline = true - } - - //check if we're still active (not timeouted) - //after initialization - if(inputMat != null) { - currentPipeline?.processFrame(inputMat)?.let { outputMat -> - if (isActive) { - pipelineFpsCounter.update() - - for (poster in pipelineOutputPosters.toTypedArray()) { - try { - poster.post(outputMat) - } catch (ex: Exception) { - Log.error( - TAG, - "Uncaught exception thrown while posting pipeline output Mat to ${poster.name} poster", - ex - ) - } - } - } - } - } - - if(!isActive) { - activePipelineContexts.remove(this.coroutineContext) - } - - updateExceptionTracker() - } catch (ex: Exception) { //handling exceptions from pipelines - updateExceptionTracker(ex) - } - } - - runBlocking { - val configTimeout = eocvSim.config.pipelineTimeout - - //allow double timeout if we haven't initialized the pipeline - val timeout = if(hasInitCurrentPipeline) { - configTimeout.ms - } else { - (configTimeout.ms * 1.8).roundToLong() - } - - try { - //ok! this is the part in which we'll wait for the pipeline with a timeout - withTimeout(timeout) { - pipelineJob.join() - } - - activePipelineContexts.remove(currentPipelineContext) - } catch (ex: TimeoutCancellationException) { - //oops, pipeline ran out of time! we'll fall back - //to default pipeline to avoid further issues. - requestForceChangePipeline(0) - //also call the event listeners in case - //someone wants to do something here - onPipelineTimeout.run() - - Log.warn(TAG , "User pipeline $currentPipelineName took too long to $lastPipelineAction (more than $timeout ms), falling back to DefaultPipeline.") - Log.blank() - } finally { - //we cancel our pipeline job so that it - //doesn't post the output mat from the - //pipeline if it ever returns. - pipelineJob.cancel() - } - } - } - - private fun updateExceptionTracker(ex: Throwable? = null) { - if(currentPipelineIndex < pipelines.size && currentPipeline != null) { - pipelineExceptionTracker.update( - pipelines[currentPipelineIndex], ex - ) - } - } - - fun callViewportTapped() = currentPipeline?.let { pipeline -> //run only if our pipeline is not null - if(paused) requestSetPaused(false) - - //similar to pipeline processFrame, call the user function in the background - //and wait for some X timeout for the user to finisih doing what it has to do. - val viewportTappedJob = GlobalScope.launch(currentPipelineContext ?: EmptyCoroutineContext) { - pipeline.onViewportTapped() - } - - val configTimeoutMs = eocvSim.config.pipelineTimeout.ms - - try { - //perform the timeout here (we'll block for a bit - //and if it runs out of time, give up and move on) - runBlocking { - withTimeout(configTimeoutMs) { - viewportTappedJob.join() - } - } - } catch(ex: TimeoutCancellationException) { - //send a warning to the user - Log.warn(TAG , "User pipeline $currentPipelineName took too long to handle onViewportTapped (more than $configTimeoutMs ms).") - } finally { - //cancel the job - viewportTappedJob.cancel() - } - } - - @JvmOverloads - fun requestAddPipelineClass(C: Class<*>, source: PipelineSource = PipelineSource.CLASSPATH) { - onUpdate.doOnce { addPipelineClass(C, source) } - } - - fun requestAddPipelineClasses(classes: List>, - source: PipelineSource = PipelineSource.CLASSPATH, - refreshGui: Boolean = false) { - onUpdate.doOnce { - for(clazz in classes) { - addPipelineClass(clazz, source) - } - if(refreshGui) refreshGuiPipelineList() - } - } - - @Suppress("UNCHECKED_CAST") - @JvmOverloads fun addPipelineClass(C: Class<*>, source: PipelineSource = PipelineSource.CLASSPATH) { - try { - pipelines.add(PipelineData(source, C as Class)) - } catch (ex: Exception) { - Log.warn(TAG, "Error while adding pipeline class", ex) - Log.warn(TAG, "Unable to cast " + C.name + " to OpenCvPipeline class.") - Log.warn(TAG, "Remember that the pipeline class should extend OpenCvPipeline") - } - } - - @JvmOverloads fun removeAllPipelinesFrom(source: PipelineSource, - refreshGuiPipelineList: Boolean = true, - changeToDefaultIfRemoved: Boolean = true) { - for(pipeline in pipelines.toTypedArray()) { - if(pipeline.source == source) { - pipelines.remove(pipeline) - - if(currentPipeline != null && currentPipeline!!::class.java == pipeline.clazz) { - if(changeToDefaultIfRemoved) - requestChangePipeline(0) //change to default pipeline if the current pipeline was deleted - } - } - } - - if(refreshGuiPipelineList) refreshGuiPipelineList() - } - - @JvmOverloads - fun requestRemoveAllPipelinesFrom(source: PipelineSource, - refreshGuiPipelineList: Boolean = true, - changeToDefaultIfRemoved: Boolean = true) { - onUpdate.doOnce { - removeAllPipelinesFrom(source, refreshGuiPipelineList, changeToDefaultIfRemoved) - } - } - - fun changePipeline(name: String, source: PipelineSource) { - for((i, data) in pipelines.withIndex()) { - if(data.clazz.simpleName.equals(name, true) && data.source == source) { - changePipeline(i) - return - } - - if(data.clazz.name.equals(name, true) && data.source == source) { - changePipeline(i) - return - } - } - - Log.warn(TAG, "Pipeline class with name $name and source $source couldn't be found") - } - - fun requestChangePipeline(name: String, source: PipelineSource) { - eocvSim.onMainUpdate.doOnce { - changePipeline(name, source) - } - } - - /** - * Changes to the requested pipeline, no matter - * if we're currently on the same pipeline or not - */ - @OptIn(ExperimentalCoroutinesApi::class) - fun forceChangePipeline(index: Int?, - applyLatestSnapshot: Boolean = false, - applyStaticSnapshot: Boolean = false) { - if(index == null) return - - captureSnapshot() - - var nextPipeline: OpenCvPipeline? - var nextTelemetry: Telemetry? - val pipelineClass = pipelines[index].clazz - - Log.info(TAG, "Changing to pipeline " + pipelineClass.name) - - var constructor: Constructor<*> - - try { - nextTelemetry = Telemetry() - - try { //instantiate pipeline if it has a constructor of a telemetry parameter - constructor = pipelineClass.getConstructor(Telemetry::class.java) - nextPipeline = constructor.newInstance(nextTelemetry) as OpenCvPipeline - } catch (ex: NoSuchMethodException) { //instantiating with a constructor of no params - constructor = pipelineClass.getConstructor() - nextPipeline = constructor.newInstance() as OpenCvPipeline - } - - Log.info(TAG, "Instantiated pipeline class " + pipelineClass.name) - } catch (ex: NoSuchMethodException) { - pipelineExceptionTracker.addMessage("Error while instantiating requested pipeline, \"${pipelineClass.simpleName}\". Falling back to previous one.") - pipelineExceptionTracker.addMessage("Make sure your pipeline implements a public constructor with no parameters or a Telemetry parameter.") - - eocvSim.visualizer.pipelineSelectorPanel.selectedIndex = currentPipelineIndex - - Log.error(TAG, "Error while instantiating requested pipeline, ${pipelineClass.simpleName} (usable constructor missing)", ex) - Log.blank() - return - } catch (ex: Exception) { - pipelineExceptionTracker.addMessage("Error while instantiating requested pipeline, \"${pipelineClass.simpleName}\". Falling back to previous one.") - updateExceptionTracker(ex) - - Log.error(TAG, "Error while instantiating requested pipeline, ${pipelineClass.simpleName} (unknown issue)", ex) - Log.blank() - - eocvSim.visualizer.pipelineSelectorPanel.selectedIndex = currentPipelineIndex - - return - } - - currentPipeline = nextPipeline - currentPipelineData = pipelines[index] - currentTelemetry = nextTelemetry - currentPipelineIndex = index - currentPipelineName = currentPipeline!!.javaClass.simpleName - - val snap = PipelineSnapshot(currentPipeline!!) - - lastInitialSnapshot = if(applyLatestSnapshot) { - applyLatestSnapshot() - snap - } else snap - - if(applyStaticSnapshot) staticSnapshot?.transferTo(currentPipeline!!) - - hasInitCurrentPipeline = false - - currentPipelineContext?.close() - currentPipelineContext = newSingleThreadContext("Pipeline-$currentPipelineName") - - activePipelineContexts.add(currentPipelineContext!!) - - eocvSim.visualizer.pipelineSelectorPanel.selectedIndex = currentPipelineIndex - - setPaused(false) - - //if pause on images option is turned on by user - if (eocvSim.configManager.config.pauseOnImages) { - //pause next frame if current selected input source is an image - eocvSim.inputSourceManager.pauseIfImageTwoFrames() - } - - onPipelineChange.run() - } - - /** - * Change to the requested pipeline only if we're - * not in the requested pipeline right now. - */ - fun changePipeline(index: Int?) { - if (index == currentPipelineIndex) return - forceChangePipeline(index) - } - - fun requestChangePipeline(index: Int?) { - onUpdate.doOnce { - changePipeline(index) - } - } - - fun requestForceChangePipeline(index: Int) = onUpdate.doOnce { forceChangePipeline(index) } - - fun applyLatestSnapshot() { - if(currentPipeline != null && latestSnapshot != null) { - latestSnapshot!!.transferTo(currentPipeline!!, lastInitialSnapshot) - } - } - - fun captureSnapshot() { - if(currentPipeline != null) { - latestSnapshot = PipelineSnapshot(currentPipeline!!) - } - } - - fun captureStaticSnapshot() { - if(currentPipeline != null) { - staticSnapshot = PipelineSnapshot(currentPipeline!!) - } - } - - fun applyStaticSnapshot(): Boolean { - staticSnapshot?.let { snap -> - onUpdate.doOnce { - val index = getIndexOf(snap.pipelineClass) - - if(index != null) { - forceChangePipeline(index, applyStaticSnapshot = true) - staticSnapshot = null - } - } - return@applyStaticSnapshot true - } - - staticSnapshot = null - return false - } - - fun getIndexOf(pipeline: OpenCvPipeline, source: PipelineSource = PipelineSource.CLASSPATH) = - getIndexOf(pipeline::class.java, source) - - fun getIndexOf(pipelineClass: Class, source: PipelineSource = PipelineSource.CLASSPATH): Int? { - for((i, pipelineData) in pipelines.withIndex()) { - if(pipelineData.clazz.name == pipelineClass.name && pipelineData.source == source) { - return i - } - } - - return null - } - - fun getPipelinesFrom(source: PipelineSource): Array { - val pipelinesData = arrayListOf() - - for(pipeline in pipelines) { - if(pipeline.source == source) - pipelinesData.add(pipeline) - } - - return pipelinesData.toTypedArray() - } - - fun runThenPause() { - setPaused(false) - eocvSim.onMainUpdate.doOnce { setPaused(true) } - } - - fun setPaused(paused: Boolean, pauseReason: PauseReason = PauseReason.USER_REQUESTED) { - this.paused = paused - - if (this.paused) { - this.pauseReason = pauseReason - onPause.run() - } else { - this.pauseReason = PauseReason.NOT_PAUSED - onResume.run() - } - - eocvSim.visualizer.pipelineSelectorPanel.buttonsPanel.pipelinePauseBtt.isSelected = paused - } - - fun togglePause() = setPaused(!paused) - - @JvmOverloads - fun requestSetPaused(paused: Boolean, pauseReason: PauseReason = PauseReason.USER_REQUESTED) { - eocvSim.onMainUpdate.doOnce { setPaused(paused, pauseReason) } - } - - fun refreshGuiPipelineList() = eocvSim.visualizer.pipelineSelectorPanel.updatePipelinesList() - -} - -enum class PipelineTimeout(val ms: Long, val coolName: String) { - LOW(1000, "Low (1 sec)"), - MEDIUM(4100, "Medium (4.1 secs)"), - HIGH(8200, "High (8.2 secs)"), - HIGHEST(12400, "Highest (12.4 secs)"); - - companion object { - @JvmStatic - fun fromCoolName(coolName: String): PipelineTimeout? { - for(timeout in values()) { - if(timeout.coolName == coolName) - return timeout - } - return null - } - } -} - -enum class PipelineFps(val fps: Int, val coolName: String) { - LOW(10, "Low (10 FPS)"), - MEDIUM(30, "Medium (30 FPS)"), - HIGH(60, "High (60 FPS)"), - HIGHEST(100, "Highest (100 FPS)"); - - companion object { - @JvmStatic - fun fromCoolName(coolName: String): PipelineFps? { - for(fps in values()) { - if(fps.coolName == coolName) - return fps - } - return null - } - } -} - -data class PipelineData(val source: PipelineSource, val clazz: Class) - -enum class PipelineSource { CLASSPATH, COMPILED_ON_RUNTIME } +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.pipeline + +import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.gui.DialogFactory +import com.github.serivesmejia.eocvsim.gui.util.MatPoster +import com.github.serivesmejia.eocvsim.pipeline.compiler.CompiledPipelineManager +import com.github.serivesmejia.eocvsim.pipeline.util.PipelineExceptionTracker +import com.github.serivesmejia.eocvsim.pipeline.util.PipelineSnapshot +import com.github.serivesmejia.eocvsim.util.Log +import com.github.serivesmejia.eocvsim.util.event.EventHandler +import com.github.serivesmejia.eocvsim.util.exception.MaxActiveContextsException +import com.github.serivesmejia.eocvsim.util.fps.FpsCounter +import kotlinx.coroutines.* +import org.firstinspires.ftc.robotcore.external.Telemetry +import org.opencv.core.Mat +import org.openftc.easyopencv.OpenCvPipeline +import org.openftc.easyopencv.TimestampedPipelineHandler +import java.lang.reflect.Constructor +import java.util.* +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.math.roundToLong + +@OptIn(DelicateCoroutinesApi::class) +class PipelineManager(var eocvSim: EOCVSim) { + + companion object { + const val MAX_ALLOWED_ACTIVE_PIPELINE_CONTEXTS = 5 + + var staticSnapshot: PipelineSnapshot? = null + private set + + private const val TAG = "PipelineManager" + } + + @JvmField val onUpdate = EventHandler("OnPipelineUpdate") + @JvmField val onPipelineChange = EventHandler("OnPipelineChange") + @JvmField val onPipelineTimeout = EventHandler("OnPipelineTimeout") + @JvmField val onPause = EventHandler("OnPipelinePause") + @JvmField val onResume = EventHandler("OnPipelineResume") + + val pipelineOutputPosters = ArrayList() + val pipelineFpsCounter = FpsCounter() + + private var hasInitCurrentPipeline = false + var lastPipelineAction = "processFrame" + private set + + val pipelines = ArrayList() + + @Volatile var currentPipeline: OpenCvPipeline? = null + private set + @Volatile var currentPipelineData: PipelineData? = null + private set + var currentPipelineName = "" + private set + var currentPipelineIndex = -1 + private set + + val activePipelineContexts = ArrayList() + private var currentPipelineContext: ExecutorCoroutineDispatcher? = null + + @Volatile var currentTelemetry: Telemetry? = null + private set + + @Volatile var paused = false + private set + get() { + if (!field) pauseReason = PauseReason.NOT_PAUSED + return field + } + + var pauseReason = PauseReason.NOT_PAUSED + private set + get() { + if (!paused) field = PauseReason.NOT_PAUSED + return field + } + + var latestSnapshot: PipelineSnapshot? = null + private set + + var lastInitialSnapshot: PipelineSnapshot? = null + private set + + //manages and builds pipelines in runtime + @JvmField val compiledPipelineManager = CompiledPipelineManager(this) + //this will be handling the special pipeline "timestamped" type + val timestampedPipelineHandler = TimestampedPipelineHandler() + //counting and tracking exceptions for logging and reporting purposes + val pipelineExceptionTracker = PipelineExceptionTracker(this) + + private var openedPipelineOutputCount = 0 + + enum class PauseReason { + USER_REQUESTED, IMAGE_ONE_ANALYSIS, NOT_PAUSED + } + + fun init() { + Log.info(TAG, "Initializing...") + + //add default pipeline + addPipelineClass(DefaultPipeline::class.java) + + //scan for pipelines + PipelineScanner(eocvSim.params.scanForPipelinesIn).lookForPipelines { + addPipelineClass(it) + } + + Log.info(TAG, "Found " + pipelines.size + " pipeline(s)") + Log.blank() + + compiledPipelineManager.init() + + // changing to initial pipeline + onUpdate.doOnce { + if(compiledPipelineManager.isBuildRunning) + compiledPipelineManager.onBuildEnd.doOnce(::applyStaticSnapOrDef) + else + applyStaticSnapOrDef() + } + + pipelineExceptionTracker.onNewPipelineException { + if(openedPipelineOutputCount <= 3) { + DialogFactory.createPipelineOutput(eocvSim) + openedPipelineOutputCount++ + } + + currentTelemetry?.errItem?.caption = "[/!\\]" + currentTelemetry?.errItem?.setValue("Uncaught exception thrown in\n pipeline, check Workspace -> Output.") + } + + pipelineExceptionTracker.onPipelineExceptionClear { + currentTelemetry?.errItem?.caption = "" + currentTelemetry?.errItem?.setValue("") + } + + onPipelineChange { + openedPipelineOutputCount = 0 + } + } + + private fun applyStaticSnapOrDef() { + onUpdate.doOnce { + if(!applyStaticSnapshot()) { + val params = eocvSim.params + + // changing to the initial pipeline, defined by the eocv sim parameters or the default pipeline + if(params.initialPipelineName != null) { + changePipeline(params.initialPipelineName!!, params.initialPipelineSource ?: PipelineSource.CLASSPATH) + } else { + forceChangePipeline(0) + } + } + + eocvSim.visualizer.pipelineSelectorPanel.allowPipelineSwitching = true + } + } + + fun update(inputMat: Mat?) { + onUpdate.run() + + if(activePipelineContexts.size > MAX_ALLOWED_ACTIVE_PIPELINE_CONTEXTS) { + throw MaxActiveContextsException("Current amount of active pipeline coroutine contexts (${activePipelineContexts.size}) is more than the maximum allowed. This generally means that there are multiple pipelines stuck in processFrame() running in the background, check for any lengthy operations in your pipelines.") + } + + if(compiledPipelineManager.isBuildRunning) { + currentTelemetry?.infoItem?.caption = "[>]" + currentTelemetry?.infoItem?.setValue("Building java files in workspace...") + } else { + currentTelemetry?.infoItem?.caption = "" + currentTelemetry?.infoItem?.setValue("") + } + + if(paused || currentPipeline == null) { + updateExceptionTracker() + return + } + + timestampedPipelineHandler.update(currentPipeline, eocvSim.inputSourceManager.currentInputSource) + + lastPipelineAction = if(!hasInitCurrentPipeline) { + "init/processFrame" + } else { + "processFrame" + } + + //run our pipeline in the background until it finishes or gets cancelled + val pipelineJob = GlobalScope.launch(currentPipelineContext!!) { + try { + //if we have a pipeline, we run it right here, passing the input mat + //given to us. we'll post the frame the pipeline returns as long + //as we haven't ran out of time (the main loop will not wait it + //forever to finish its job). if we run out of time, and if the + //pipeline ever returns, we will not post the frame, since we + //don't know when it was actually requested, we might even be in + //a different pipeline at this point. we also call init if we + //haven't done so. + + if(!hasInitCurrentPipeline && inputMat != null) { + currentPipeline?.init(inputMat) + + Log.info("PipelineManager", "Initialized pipeline $currentPipelineName") + Log.blank() + + hasInitCurrentPipeline = true + } + + //check if we're still active (not timeouted) + //after initialization + if(inputMat != null) { + currentPipeline?.processFrame(inputMat)?.let { outputMat -> + if (isActive) { + pipelineFpsCounter.update() + + for (poster in pipelineOutputPosters.toTypedArray()) { + try { + poster.post(outputMat) + } catch (ex: Exception) { + Log.error( + TAG, + "Uncaught exception thrown while posting pipeline output Mat to ${poster.name} poster", + ex + ) + } + } + } + } + } + + if(!isActive) { + activePipelineContexts.remove(this.coroutineContext) + } + + updateExceptionTracker() + } catch (ex: Exception) { //handling exceptions from pipelines + updateExceptionTracker(ex) + } + } + + runBlocking { + val configTimeout = eocvSim.config.pipelineTimeout + + //allow double timeout if we haven't initialized the pipeline + val timeout = if(hasInitCurrentPipeline) { + configTimeout.ms + } else { + (configTimeout.ms * 1.8).roundToLong() + } + + try { + //ok! this is the part in which we'll wait for the pipeline with a timeout + withTimeout(timeout) { + pipelineJob.join() + } + + activePipelineContexts.remove(currentPipelineContext) + } catch (ex: TimeoutCancellationException) { + //oops, pipeline ran out of time! we'll fall back + //to default pipeline to avoid further issues. + requestForceChangePipeline(0) + //also call the event listeners in case + //someone wants to do something here + onPipelineTimeout.run() + + Log.warn(TAG , "User pipeline $currentPipelineName took too long to $lastPipelineAction (more than $timeout ms), falling back to DefaultPipeline.") + Log.blank() + } finally { + //we cancel our pipeline job so that it + //doesn't post the output mat from the + //pipeline if it ever returns. + pipelineJob.cancel() + } + } + } + + private fun updateExceptionTracker(ex: Throwable? = null) { + if(currentPipelineIndex < pipelines.size && currentPipeline != null) { + pipelineExceptionTracker.update( + pipelines[currentPipelineIndex], ex + ) + } + } + + fun callViewportTapped() = currentPipeline?.let { pipeline -> //run only if our pipeline is not null + if(paused) requestSetPaused(false) + + //similar to pipeline processFrame, call the user function in the background + //and wait for some X timeout for the user to finisih doing what it has to do. + val viewportTappedJob = GlobalScope.launch(currentPipelineContext ?: EmptyCoroutineContext) { + pipeline.onViewportTapped() + } + + val configTimeoutMs = eocvSim.config.pipelineTimeout.ms + + try { + //perform the timeout here (we'll block for a bit + //and if it runs out of time, give up and move on) + runBlocking { + withTimeout(configTimeoutMs) { + viewportTappedJob.join() + } + } + } catch(ex: TimeoutCancellationException) { + //send a warning to the user + Log.warn(TAG , "User pipeline $currentPipelineName took too long to handle onViewportTapped (more than $configTimeoutMs ms).") + } finally { + //cancel the job + viewportTappedJob.cancel() + } + } + + @JvmOverloads + fun requestAddPipelineClass(C: Class<*>, source: PipelineSource = PipelineSource.CLASSPATH) { + onUpdate.doOnce { addPipelineClass(C, source) } + } + + fun requestAddPipelineClasses(classes: List>, + source: PipelineSource = PipelineSource.CLASSPATH, + refreshGui: Boolean = false) { + onUpdate.doOnce { + for(clazz in classes) { + addPipelineClass(clazz, source) + } + if(refreshGui) refreshGuiPipelineList() + } + } + + @Suppress("UNCHECKED_CAST") + @JvmOverloads fun addPipelineClass(C: Class<*>, source: PipelineSource = PipelineSource.CLASSPATH) { + try { + pipelines.add(PipelineData(source, C as Class)) + } catch (ex: Exception) { + Log.warn(TAG, "Error while adding pipeline class", ex) + Log.warn(TAG, "Unable to cast " + C.name + " to OpenCvPipeline class.") + Log.warn(TAG, "Remember that the pipeline class should extend OpenCvPipeline") + } + } + + @JvmOverloads fun removeAllPipelinesFrom(source: PipelineSource, + refreshGuiPipelineList: Boolean = true, + changeToDefaultIfRemoved: Boolean = true) { + for(pipeline in pipelines.toTypedArray()) { + if(pipeline.source == source) { + pipelines.remove(pipeline) + + if(currentPipeline != null && currentPipeline!!::class.java == pipeline.clazz) { + if(changeToDefaultIfRemoved) + requestChangePipeline(0) //change to default pipeline if the current pipeline was deleted + } + } + } + + if(refreshGuiPipelineList) refreshGuiPipelineList() + } + + @JvmOverloads + fun requestRemoveAllPipelinesFrom(source: PipelineSource, + refreshGuiPipelineList: Boolean = true, + changeToDefaultIfRemoved: Boolean = true) { + onUpdate.doOnce { + removeAllPipelinesFrom(source, refreshGuiPipelineList, changeToDefaultIfRemoved) + } + } + + fun changePipeline(name: String, source: PipelineSource) { + for((i, data) in pipelines.withIndex()) { + if(data.clazz.simpleName.equals(name, true) && data.source == source) { + changePipeline(i) + return + } + + if(data.clazz.name.equals(name, true) && data.source == source) { + changePipeline(i) + return + } + } + + Log.warn(TAG, "Pipeline class with name $name and source $source couldn't be found") + } + + fun requestChangePipeline(name: String, source: PipelineSource) { + eocvSim.onMainUpdate.doOnce { + changePipeline(name, source) + } + } + + /** + * Changes to the requested pipeline, no matter + * if we're currently on the same pipeline or not + */ + @OptIn(ExperimentalCoroutinesApi::class) + fun forceChangePipeline(index: Int?, + applyLatestSnapshot: Boolean = false, + applyStaticSnapshot: Boolean = false) { + if(index == null) return + + captureSnapshot() + + var nextPipeline: OpenCvPipeline? + var nextTelemetry: Telemetry? + val pipelineClass = pipelines[index].clazz + + Log.info(TAG, "Changing to pipeline " + pipelineClass.name) + + var constructor: Constructor<*> + + try { + nextTelemetry = Telemetry() + + try { //instantiate pipeline if it has a constructor of a telemetry parameter + constructor = pipelineClass.getConstructor(Telemetry::class.java) + nextPipeline = constructor.newInstance(nextTelemetry) as OpenCvPipeline + } catch (ex: NoSuchMethodException) { //instantiating with a constructor of no params + constructor = pipelineClass.getConstructor() + nextPipeline = constructor.newInstance() as OpenCvPipeline + } + + Log.info(TAG, "Instantiated pipeline class " + pipelineClass.name) + } catch (ex: NoSuchMethodException) { + pipelineExceptionTracker.addMessage("Error while instantiating requested pipeline, \"${pipelineClass.simpleName}\". Falling back to previous one.") + pipelineExceptionTracker.addMessage("Make sure your pipeline implements a public constructor with no parameters or a Telemetry parameter.") + + eocvSim.visualizer.pipelineSelectorPanel.selectedIndex = currentPipelineIndex + + Log.error(TAG, "Error while instantiating requested pipeline, ${pipelineClass.simpleName} (usable constructor missing)", ex) + Log.blank() + return + } catch (ex: Exception) { + pipelineExceptionTracker.addMessage("Error while instantiating requested pipeline, \"${pipelineClass.simpleName}\". Falling back to previous one.") + updateExceptionTracker(ex) + + Log.error(TAG, "Error while instantiating requested pipeline, ${pipelineClass.simpleName} (unknown issue)", ex) + Log.blank() + + eocvSim.visualizer.pipelineSelectorPanel.selectedIndex = currentPipelineIndex + + return + } + + currentPipeline = nextPipeline + currentPipelineData = pipelines[index] + currentTelemetry = nextTelemetry + currentPipelineIndex = index + currentPipelineName = currentPipeline!!.javaClass.simpleName + + val snap = PipelineSnapshot(currentPipeline!!) + + lastInitialSnapshot = if(applyLatestSnapshot) { + applyLatestSnapshot() + snap + } else snap + + if(applyStaticSnapshot) staticSnapshot?.transferTo(currentPipeline!!) + + hasInitCurrentPipeline = false + + currentPipelineContext?.close() + currentPipelineContext = newSingleThreadContext("Pipeline-$currentPipelineName") + + activePipelineContexts.add(currentPipelineContext!!) + + eocvSim.visualizer.pipelineSelectorPanel.selectedIndex = currentPipelineIndex + + setPaused(false) + + //if pause on images option is turned on by user + if (eocvSim.configManager.config.pauseOnImages) { + //pause next frame if current selected input source is an image + eocvSim.inputSourceManager.pauseIfImageTwoFrames() + } + + onPipelineChange.run() + } + + /** + * Change to the requested pipeline only if we're + * not in the requested pipeline right now. + */ + fun changePipeline(index: Int?) { + if (index == currentPipelineIndex) return + forceChangePipeline(index) + } + + fun requestChangePipeline(index: Int?) { + onUpdate.doOnce { + changePipeline(index) + } + } + + fun requestForceChangePipeline(index: Int) = onUpdate.doOnce { forceChangePipeline(index) } + + fun applyLatestSnapshot() { + if(currentPipeline != null && latestSnapshot != null) { + latestSnapshot!!.transferTo(currentPipeline!!, lastInitialSnapshot) + } + } + + fun captureSnapshot() { + if(currentPipeline != null) { + latestSnapshot = PipelineSnapshot(currentPipeline!!) + } + } + + fun captureStaticSnapshot() { + if(currentPipeline != null) { + staticSnapshot = PipelineSnapshot(currentPipeline!!) + } + } + + fun applyStaticSnapshot(): Boolean { + staticSnapshot?.let { snap -> + onUpdate.doOnce { + val index = getIndexOf(snap.pipelineClass) + + if(index != null) { + forceChangePipeline(index, applyStaticSnapshot = true) + staticSnapshot = null + } + } + return@applyStaticSnapshot true + } + + staticSnapshot = null + return false + } + + fun getIndexOf(pipeline: OpenCvPipeline, source: PipelineSource = PipelineSource.CLASSPATH) = + getIndexOf(pipeline::class.java, source) + + fun getIndexOf(pipelineClass: Class, source: PipelineSource = PipelineSource.CLASSPATH): Int? { + for((i, pipelineData) in pipelines.withIndex()) { + if(pipelineData.clazz.name == pipelineClass.name && pipelineData.source == source) { + return i + } + } + + return null + } + + fun getPipelinesFrom(source: PipelineSource): Array { + val pipelinesData = arrayListOf() + + for(pipeline in pipelines) { + if(pipeline.source == source) + pipelinesData.add(pipeline) + } + + return pipelinesData.toTypedArray() + } + + fun runThenPause() { + setPaused(false) + eocvSim.onMainUpdate.doOnce { setPaused(true) } + } + + fun setPaused(paused: Boolean, pauseReason: PauseReason = PauseReason.USER_REQUESTED) { + this.paused = paused + + if (this.paused) { + this.pauseReason = pauseReason + onPause.run() + } else { + this.pauseReason = PauseReason.NOT_PAUSED + onResume.run() + } + + eocvSim.visualizer.pipelineSelectorPanel.buttonsPanel.pipelinePauseBtt.isSelected = paused + } + + fun togglePause() = setPaused(!paused) + + @JvmOverloads + fun requestSetPaused(paused: Boolean, pauseReason: PauseReason = PauseReason.USER_REQUESTED) { + eocvSim.onMainUpdate.doOnce { setPaused(paused, pauseReason) } + } + + fun refreshGuiPipelineList() = eocvSim.visualizer.pipelineSelectorPanel.updatePipelinesList() + +} + +enum class PipelineTimeout(val ms: Long, val coolName: String) { + LOW(1000, "Low (1 sec)"), + MEDIUM(4100, "Medium (4.1 secs)"), + HIGH(8200, "High (8.2 secs)"), + HIGHEST(12400, "Highest (12.4 secs)"); + + companion object { + @JvmStatic + fun fromCoolName(coolName: String): PipelineTimeout? { + for(timeout in values()) { + if(timeout.coolName == coolName) + return timeout + } + return null + } + } +} + +enum class PipelineFps(val fps: Int, val coolName: String) { + LOW(10, "Low (10 FPS)"), + MEDIUM(30, "Medium (30 FPS)"), + HIGH(60, "High (60 FPS)"), + HIGHEST(100, "Highest (100 FPS)"); + + companion object { + @JvmStatic + fun fromCoolName(coolName: String): PipelineFps? { + for(fps in values()) { + if(fps.coolName == coolName) + return fps + } + return null + } + } +} + +data class PipelineData(val source: PipelineSource, val clazz: Class) + +enum class PipelineSource { CLASSPATH, COMPILED_ON_RUNTIME } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineScanner.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineScanner.kt index 8131cde0..8a6df940 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineScanner.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineScanner.kt @@ -1,63 +1,63 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.pipeline - -import com.github.serivesmejia.eocvsim.util.Log -import com.github.serivesmejia.eocvsim.util.ReflectUtil -import io.github.classgraph.ClassGraph -import io.github.classgraph.ScanResult -import org.openftc.easyopencv.OpenCvPipeline - -@Suppress("UNCHECKED_CAST") -class PipelineScanner(val scanInPackage: String = "org.firstinspires") { - - fun lookForPipelines(callback: (Class) -> Unit) { - Log.info("PipelineScanner", "Scanning for pipelines...") - val scanResult = scanClasspath(scanInPackage) - - //iterate over the results of the scan - for (routeClassInfo in scanResult.allClasses) { - - val foundClass: Class<*> = try { - Class.forName(routeClassInfo.name) - } catch (e1: ClassNotFoundException) { - e1.printStackTrace() - continue //continue because we couldn't get the class... - } - - if(ReflectUtil.hasSuperclass(foundClass, OpenCvPipeline::class.java)) { - Log.info("PipelineScanner", "Found pipeline class ${foundClass.canonicalName}") - callback(foundClass as Class); - } - - } - } - - fun scanClasspath(inPackage: String): ScanResult { - //Scan for all classes in the specified package - val classGraph = ClassGraph().enableAllInfo().acceptPackages(inPackage) - return classGraph.scan() - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.pipeline + +import com.github.serivesmejia.eocvsim.util.Log +import com.github.serivesmejia.eocvsim.util.ReflectUtil +import io.github.classgraph.ClassGraph +import io.github.classgraph.ScanResult +import org.openftc.easyopencv.OpenCvPipeline + +@Suppress("UNCHECKED_CAST") +class PipelineScanner(val scanInPackage: String = "org.firstinspires") { + + fun lookForPipelines(callback: (Class) -> Unit) { + Log.info("PipelineScanner", "Scanning for pipelines...") + val scanResult = scanClasspath(scanInPackage) + + //iterate over the results of the scan + for (routeClassInfo in scanResult.allClasses) { + + val foundClass: Class<*> = try { + Class.forName(routeClassInfo.name) + } catch (e1: ClassNotFoundException) { + e1.printStackTrace() + continue //continue because we couldn't get the class... + } + + if(ReflectUtil.hasSuperclass(foundClass, OpenCvPipeline::class.java)) { + Log.info("PipelineScanner", "Found pipeline class ${foundClass.canonicalName}") + callback(foundClass as Class); + } + + } + } + + fun scanClasspath(inPackage: String): ScanResult { + //Scan for all classes in the specified package + val classGraph = ClassGraph().enableAllInfo().acceptPackages(inPackage) + return classGraph.scan() + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt index 96420ea1..fd00895a 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/CompiledPipelineManager.kt @@ -1,261 +1,261 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.pipeline.compiler - -import com.github.serivesmejia.eocvsim.gui.DialogFactory -import com.github.serivesmejia.eocvsim.gui.dialog.Output -import com.github.serivesmejia.eocvsim.pipeline.PipelineManager -import com.github.serivesmejia.eocvsim.pipeline.PipelineSource -import com.github.serivesmejia.eocvsim.util.Log -import com.github.serivesmejia.eocvsim.util.StrUtil -import com.github.serivesmejia.eocvsim.util.SysUtil -import com.github.serivesmejia.eocvsim.util.event.EventHandler -import com.github.serivesmejia.eocvsim.workspace.util.template.DefaultWorkspaceTemplate -import com.qualcomm.robotcore.util.ElapsedTime -import kotlinx.coroutines.* -import org.openftc.easyopencv.OpenCvPipeline -import java.io.File - -class CompiledPipelineManager(private val pipelineManager: PipelineManager) { - - companion object { - val DEF_WORKSPACE_FOLDER = File(SysUtil.getEOCVSimFolder(), File.separator + "default_workspace").apply { - if(!exists()) { - mkdir() - DefaultWorkspaceTemplate.extractToIfEmpty(this) - } - } - - val COMPILER_FOLDER = File(SysUtil.getEOCVSimFolder(), File.separator + "compiler").mkdirLazy() - - val SOURCES_OUTPUT_FOLDER = File(COMPILER_FOLDER, File.separator + "gen_src").mkdirLazy() - val CLASSES_OUTPUT_FOLDER = File(COMPILER_FOLDER, File.separator + "out_classes").mkdirLazy() - val JARS_OUTPUT_FOLDER = File(COMPILER_FOLDER, File.separator + "out_jars").mkdirLazy() - - val PIPELINES_OUTPUT_JAR = File(JARS_OUTPUT_FOLDER, File.separator + "pipelines.jar") - - const val TAG = "CompiledPipelineManager" - } - - var currentPipelineClassLoader: PipelineClassLoader? = null - private set - - val onBuildStart = EventHandler("CompiledPipelineManager-OnBuildStart") - val onBuildEnd = EventHandler("CompiledPipelineManager-OnBuildEnd") - - var lastBuildResult: PipelineCompileResult? = null - private set - var lastBuildOutputMessage: String? = null - private set - - var isBuildRunning = false - private set - - val workspaceManager get() = pipelineManager.eocvSim.workspaceManager - - fun init() { - Log.info(TAG, "Initializing...") - asyncCompile(false) - - workspaceManager.onWorkspaceChange { - asyncCompile() - } - } - - @OptIn(DelicateCoroutinesApi::class) - suspend fun uncheckedCompile(fixSelectedPipeline: Boolean = false): PipelineCompileResult { - if(isBuildRunning) return PipelineCompileResult( - PipelineCompileStatus.FAILED, "A build is already running" - ) - - isBuildRunning = true - onBuildStart.run() - - if(!PipelineCompiler.IS_USABLE) { - lastBuildResult = PipelineCompileResult( - PipelineCompileStatus.FAILED, - "Current JVM does not have a javac executable (a JDK is needed)" - ) - lastBuildOutputMessage = null - - onBuildEnd.run() - isBuildRunning = false - - return lastBuildResult!! - } - - workspaceManager.reloadConfig() - - val absoluteSourcesPath = workspaceManager.sourcesAbsolutePath.toFile() - Log.info(TAG, "Building java files in workspace, at ${absoluteSourcesPath.absolutePath}") - - val runtime = ElapsedTime() - - val compiler = PipelineCompiler( - absoluteSourcesPath, workspaceManager.sourceFiles, - workspaceManager.resourcesAbsolutePath.toFile(), workspaceManager.resourceFiles - ) - - val result = compiler.compile(PIPELINES_OUTPUT_JAR) - lastBuildResult = result - - val timeElapsed = String.format("%.2f", runtime.seconds()) - - currentPipelineClassLoader = null - val messageEnd = "(took $timeElapsed seconds)\n\n${result.message}".trim() - - val pipelineSelectorPanel = pipelineManager.eocvSim.visualizer.pipelineSelectorPanel - val beforeAllowSwitching = pipelineSelectorPanel?.allowPipelineSwitching - - if(fixSelectedPipeline) - pipelineSelectorPanel?.allowPipelineSwitching = false - - pipelineManager.requestRemoveAllPipelinesFrom( - PipelineSource.COMPILED_ON_RUNTIME, - refreshGuiPipelineList = false, - changeToDefaultIfRemoved = false - ) - - lastBuildOutputMessage = when(result.status) { - PipelineCompileStatus.SUCCESS -> { - loadFromPipelinesJar() - "Build successful $messageEnd" - } - PipelineCompileStatus.NO_SOURCE -> { - //delete jar if we had no sources, the most logical outcome in this case - deleteJarFile() - if(pipelineManager.eocvSim.visualizer.hasFinishedInit()) - pipelineManager.refreshGuiPipelineList() - - "Build cancelled, no source files to compile $messageEnd" - } - else -> { - deleteJarFile() - "Build failed $messageEnd" - } - } - - val beforePipeline = pipelineManager.currentPipelineData - - pipelineManager.onUpdate.doOnce { - pipelineManager.refreshGuiPipelineList() - - if(fixSelectedPipeline) { - if(beforePipeline != null) { - val pipeline = pipelineManager.getIndexOf(beforePipeline.clazz, beforePipeline.source) - - pipelineManager.forceChangePipeline(pipeline, true) - } else { - pipelineManager.changePipeline(0) //default pipeline - } - - pipelineSelectorPanel?.allowPipelineSwitching = beforeAllowSwitching!! - } - } - - if(result.status == PipelineCompileStatus.SUCCESS) { - Log.info(TAG, "$lastBuildOutputMessage\n") - } else { - Log.warn(TAG, "$lastBuildOutputMessage\n") - - if(result.status == PipelineCompileStatus.FAILED && !Output.isAlreadyOpened) - DialogFactory.createBuildOutput(pipelineManager.eocvSim) - } - - onBuildEnd.callRightAway = true - onBuildEnd.run() - - GlobalScope.launch { - delay(1000) - onBuildEnd.callRightAway = false - } - - isBuildRunning = false - - return result - } - - fun compile(fixSelectedPipeline: Boolean = true) = try { - runBlocking { uncheckedCompile(fixSelectedPipeline) } - } catch(e: Throwable) { - isBuildRunning = false - onBuildEnd.run() - - val stacktrace = StrUtil.fromException(e) - lastBuildOutputMessage = """ - |Unexpected exception thrown while the build was running - | - |$stacktrace - | - |If this seems like a bug, please open an issue in the EOCV-Sim github repo - """.trimMargin() - - Log.error(TAG, lastBuildOutputMessage) - - lastBuildResult = PipelineCompileResult(PipelineCompileStatus.FAILED, lastBuildOutputMessage!!) - - if(!Output.isAlreadyOpened) - DialogFactory.createBuildOutput(pipelineManager.eocvSim) - - lastBuildResult!! - } - - @JvmOverloads - @OptIn(DelicateCoroutinesApi::class) - fun asyncCompile( - fixSelectedPipeline: Boolean = true, - endCallback: (PipelineCompileResult) -> Unit = {} - ) = GlobalScope.launch(Dispatchers.IO) { - endCallback(compile(fixSelectedPipeline)) - } - - private fun deleteJarFile() { - if(PIPELINES_OUTPUT_JAR.exists()) PIPELINES_OUTPUT_JAR.delete() - currentPipelineClassLoader = null - } - - fun loadFromPipelinesJar() { - if(!PIPELINES_OUTPUT_JAR.exists()) return - - Log.info(TAG, "Looking for pipelines in jar file $PIPELINES_OUTPUT_JAR") - - try { - currentPipelineClassLoader = PipelineClassLoader(PIPELINES_OUTPUT_JAR) - - val pipelines = mutableListOf>() - - for(pipelineClass in currentPipelineClassLoader!!.pipelineClasses) { - pipelines.add(pipelineClass) - Log.info(TAG, "Added ${pipelineClass.simpleName} from jar") - } - - pipelineManager.requestAddPipelineClasses(pipelines, PipelineSource.COMPILED_ON_RUNTIME, false) - } catch(e: Exception) { - Log.error(TAG, "Uncaught exception thrown while loading jar $PIPELINES_OUTPUT_JAR", e) - } - } - -} - -private fun File.mkdirLazy() = apply { mkdir() } +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.pipeline.compiler + +import com.github.serivesmejia.eocvsim.gui.DialogFactory +import com.github.serivesmejia.eocvsim.gui.dialog.Output +import com.github.serivesmejia.eocvsim.pipeline.PipelineManager +import com.github.serivesmejia.eocvsim.pipeline.PipelineSource +import com.github.serivesmejia.eocvsim.util.Log +import com.github.serivesmejia.eocvsim.util.StrUtil +import com.github.serivesmejia.eocvsim.util.SysUtil +import com.github.serivesmejia.eocvsim.util.event.EventHandler +import com.github.serivesmejia.eocvsim.workspace.util.template.DefaultWorkspaceTemplate +import com.qualcomm.robotcore.util.ElapsedTime +import kotlinx.coroutines.* +import org.openftc.easyopencv.OpenCvPipeline +import java.io.File + +class CompiledPipelineManager(private val pipelineManager: PipelineManager) { + + companion object { + val DEF_WORKSPACE_FOLDER = File(SysUtil.getEOCVSimFolder(), File.separator + "default_workspace").apply { + if(!exists()) { + mkdir() + DefaultWorkspaceTemplate.extractToIfEmpty(this) + } + } + + val COMPILER_FOLDER = File(SysUtil.getEOCVSimFolder(), File.separator + "compiler").mkdirLazy() + + val SOURCES_OUTPUT_FOLDER = File(COMPILER_FOLDER, File.separator + "gen_src").mkdirLazy() + val CLASSES_OUTPUT_FOLDER = File(COMPILER_FOLDER, File.separator + "out_classes").mkdirLazy() + val JARS_OUTPUT_FOLDER = File(COMPILER_FOLDER, File.separator + "out_jars").mkdirLazy() + + val PIPELINES_OUTPUT_JAR = File(JARS_OUTPUT_FOLDER, File.separator + "pipelines.jar") + + const val TAG = "CompiledPipelineManager" + } + + var currentPipelineClassLoader: PipelineClassLoader? = null + private set + + val onBuildStart = EventHandler("CompiledPipelineManager-OnBuildStart") + val onBuildEnd = EventHandler("CompiledPipelineManager-OnBuildEnd") + + var lastBuildResult: PipelineCompileResult? = null + private set + var lastBuildOutputMessage: String? = null + private set + + var isBuildRunning = false + private set + + val workspaceManager get() = pipelineManager.eocvSim.workspaceManager + + fun init() { + Log.info(TAG, "Initializing...") + asyncCompile(false) + + workspaceManager.onWorkspaceChange { + asyncCompile() + } + } + + @OptIn(DelicateCoroutinesApi::class) + suspend fun uncheckedCompile(fixSelectedPipeline: Boolean = false): PipelineCompileResult { + if(isBuildRunning) return PipelineCompileResult( + PipelineCompileStatus.FAILED, "A build is already running" + ) + + isBuildRunning = true + onBuildStart.run() + + if(!PipelineCompiler.IS_USABLE) { + lastBuildResult = PipelineCompileResult( + PipelineCompileStatus.FAILED, + "Current JVM does not have a javac executable (a JDK is needed)" + ) + lastBuildOutputMessage = null + + onBuildEnd.run() + isBuildRunning = false + + return lastBuildResult!! + } + + workspaceManager.reloadConfig() + + val absoluteSourcesPath = workspaceManager.sourcesAbsolutePath.toFile() + Log.info(TAG, "Building java files in workspace, at ${absoluteSourcesPath.absolutePath}") + + val runtime = ElapsedTime() + + val compiler = PipelineCompiler( + absoluteSourcesPath, workspaceManager.sourceFiles, + workspaceManager.resourcesAbsolutePath.toFile(), workspaceManager.resourceFiles + ) + + val result = compiler.compile(PIPELINES_OUTPUT_JAR) + lastBuildResult = result + + val timeElapsed = String.format("%.2f", runtime.seconds()) + + currentPipelineClassLoader = null + val messageEnd = "(took $timeElapsed seconds)\n\n${result.message}".trim() + + val pipelineSelectorPanel = pipelineManager.eocvSim.visualizer.pipelineSelectorPanel + val beforeAllowSwitching = pipelineSelectorPanel?.allowPipelineSwitching + + if(fixSelectedPipeline) + pipelineSelectorPanel?.allowPipelineSwitching = false + + pipelineManager.requestRemoveAllPipelinesFrom( + PipelineSource.COMPILED_ON_RUNTIME, + refreshGuiPipelineList = false, + changeToDefaultIfRemoved = false + ) + + lastBuildOutputMessage = when(result.status) { + PipelineCompileStatus.SUCCESS -> { + loadFromPipelinesJar() + "Build successful $messageEnd" + } + PipelineCompileStatus.NO_SOURCE -> { + //delete jar if we had no sources, the most logical outcome in this case + deleteJarFile() + if(pipelineManager.eocvSim.visualizer.hasFinishedInit()) + pipelineManager.refreshGuiPipelineList() + + "Build cancelled, no source files to compile $messageEnd" + } + else -> { + deleteJarFile() + "Build failed $messageEnd" + } + } + + val beforePipeline = pipelineManager.currentPipelineData + + pipelineManager.onUpdate.doOnce { + pipelineManager.refreshGuiPipelineList() + + if(fixSelectedPipeline) { + if(beforePipeline != null) { + val pipeline = pipelineManager.getIndexOf(beforePipeline.clazz, beforePipeline.source) + + pipelineManager.forceChangePipeline(pipeline, true) + } else { + pipelineManager.changePipeline(0) //default pipeline + } + + pipelineSelectorPanel?.allowPipelineSwitching = beforeAllowSwitching!! + } + } + + if(result.status == PipelineCompileStatus.SUCCESS) { + Log.info(TAG, "$lastBuildOutputMessage\n") + } else { + Log.warn(TAG, "$lastBuildOutputMessage\n") + + if(result.status == PipelineCompileStatus.FAILED && !Output.isAlreadyOpened) + DialogFactory.createBuildOutput(pipelineManager.eocvSim) + } + + onBuildEnd.callRightAway = true + onBuildEnd.run() + + GlobalScope.launch { + delay(1000) + onBuildEnd.callRightAway = false + } + + isBuildRunning = false + + return result + } + + fun compile(fixSelectedPipeline: Boolean = true) = try { + runBlocking { uncheckedCompile(fixSelectedPipeline) } + } catch(e: Throwable) { + isBuildRunning = false + onBuildEnd.run() + + val stacktrace = StrUtil.fromException(e) + lastBuildOutputMessage = """ + |Unexpected exception thrown while the build was running + | + |$stacktrace + | + |If this seems like a bug, please open an issue in the EOCV-Sim github repo + """.trimMargin() + + Log.error(TAG, lastBuildOutputMessage) + + lastBuildResult = PipelineCompileResult(PipelineCompileStatus.FAILED, lastBuildOutputMessage!!) + + if(!Output.isAlreadyOpened) + DialogFactory.createBuildOutput(pipelineManager.eocvSim) + + lastBuildResult!! + } + + @JvmOverloads + @OptIn(DelicateCoroutinesApi::class) + fun asyncCompile( + fixSelectedPipeline: Boolean = true, + endCallback: (PipelineCompileResult) -> Unit = {} + ) = GlobalScope.launch(Dispatchers.IO) { + endCallback(compile(fixSelectedPipeline)) + } + + private fun deleteJarFile() { + if(PIPELINES_OUTPUT_JAR.exists()) PIPELINES_OUTPUT_JAR.delete() + currentPipelineClassLoader = null + } + + fun loadFromPipelinesJar() { + if(!PIPELINES_OUTPUT_JAR.exists()) return + + Log.info(TAG, "Looking for pipelines in jar file $PIPELINES_OUTPUT_JAR") + + try { + currentPipelineClassLoader = PipelineClassLoader(PIPELINES_OUTPUT_JAR) + + val pipelines = mutableListOf>() + + for(pipelineClass in currentPipelineClassLoader!!.pipelineClasses) { + pipelines.add(pipelineClass) + Log.info(TAG, "Added ${pipelineClass.simpleName} from jar") + } + + pipelineManager.requestAddPipelineClasses(pipelines, PipelineSource.COMPILED_ON_RUNTIME, false) + } catch(e: Exception) { + Log.error(TAG, "Uncaught exception thrown while loading jar $PIPELINES_OUTPUT_JAR", e) + } + } + +} + +private fun File.mkdirLazy() = apply { mkdir() } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineClassLoader.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineClassLoader.kt index 342645a6..567990ba 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineClassLoader.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineClassLoader.kt @@ -1,103 +1,103 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.pipeline.compiler - -import com.github.serivesmejia.eocvsim.util.ReflectUtil -import com.github.serivesmejia.eocvsim.util.SysUtil -import com.github.serivesmejia.eocvsim.util.extension.removeFromEnd -import org.openftc.easyopencv.OpenCvPipeline -import java.io.* -import java.util.zip.ZipEntry -import java.util.zip.ZipFile - -@Suppress("UNCHECKED_CAST") -class PipelineClassLoader(pipelinesJar: File) : ClassLoader() { - - private val zipFile = ZipFile(pipelinesJar) - - var pipelineClasses: List> - private set - - init { - val pipelineClasses = mutableListOf>() - - for(entry in zipFile.entries()) { - if(!entry.name.endsWith(".class")) continue - - val clazz = loadClass(entry) - - if(ReflectUtil.hasSuperclass(clazz, OpenCvPipeline::class.java)) { - pipelineClasses.add(clazz as Class) - } - } - - this.pipelineClasses = pipelineClasses.toList() - } - - private fun loadClass(entry: ZipEntry): Class<*> { - val name = entry.name.removeFromEnd(".class").replace(File.separatorChar, '.') - - zipFile.getInputStream(entry).use { inStream -> - ByteArrayOutputStream().use { outStream -> - SysUtil.copyStream(inStream, outStream) - val bytes = outStream.toByteArray() - - return defineClass(name, bytes, 0, bytes.size) - } - } - } - - override fun loadClass(name: String, resolve: Boolean): Class<*> { - var clazz = findLoadedClass(name) - - if(clazz == null) { - try { - clazz = loadClass(zipFile.getEntry(name.replace('.', File.separatorChar) + ".class")) - if(resolve) resolveClass(clazz) - } catch(e: Exception) { - clazz = super.loadClass(name, resolve) - } - } - - return clazz - } - - override fun getResourceAsStream(name: String): InputStream? { - println("trying to load $name") - - val entry = zipFile.getEntry(name) - - if(entry != null) { - try { - return zipFile.getInputStream(entry) - } catch (e: IOException) { } - } - - return super.getResourceAsStream(name) - } - -} - -val OpenCvPipeline.isFromRuntimeCompilation +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.pipeline.compiler + +import com.github.serivesmejia.eocvsim.util.ReflectUtil +import com.github.serivesmejia.eocvsim.util.SysUtil +import com.github.serivesmejia.eocvsim.util.extension.removeFromEnd +import org.openftc.easyopencv.OpenCvPipeline +import java.io.* +import java.util.zip.ZipEntry +import java.util.zip.ZipFile + +@Suppress("UNCHECKED_CAST") +class PipelineClassLoader(pipelinesJar: File) : ClassLoader() { + + private val zipFile = ZipFile(pipelinesJar) + + var pipelineClasses: List> + private set + + init { + val pipelineClasses = mutableListOf>() + + for(entry in zipFile.entries()) { + if(!entry.name.endsWith(".class")) continue + + val clazz = loadClass(entry) + + if(ReflectUtil.hasSuperclass(clazz, OpenCvPipeline::class.java)) { + pipelineClasses.add(clazz as Class) + } + } + + this.pipelineClasses = pipelineClasses.toList() + } + + private fun loadClass(entry: ZipEntry): Class<*> { + val name = entry.name.removeFromEnd(".class").replace(File.separatorChar, '.') + + zipFile.getInputStream(entry).use { inStream -> + ByteArrayOutputStream().use { outStream -> + SysUtil.copyStream(inStream, outStream) + val bytes = outStream.toByteArray() + + return defineClass(name, bytes, 0, bytes.size) + } + } + } + + override fun loadClass(name: String, resolve: Boolean): Class<*> { + var clazz = findLoadedClass(name) + + if(clazz == null) { + try { + clazz = loadClass(zipFile.getEntry(name.replace('.', File.separatorChar) + ".class")) + if(resolve) resolveClass(clazz) + } catch(e: Exception) { + clazz = super.loadClass(name, resolve) + } + } + + return clazz + } + + override fun getResourceAsStream(name: String): InputStream? { + println("trying to load $name") + + val entry = zipFile.getEntry(name) + + if(entry != null) { + try { + return zipFile.getInputStream(entry) + } catch (e: IOException) { } + } + + return super.getResourceAsStream(name) + } + +} + +val OpenCvPipeline.isFromRuntimeCompilation get() = this::class.java.classLoader is PipelineClassLoader \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineCompiler.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineCompiler.kt index 13b2bd7d..bae0fe06 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineCompiler.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineCompiler.kt @@ -1,149 +1,149 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.pipeline.compiler - -import com.github.serivesmejia.eocvsim.util.Log -import com.github.serivesmejia.eocvsim.util.SysUtil -import com.github.serivesmejia.eocvsim.util.compiler.JarPacker -import java.io.File -import java.io.PrintWriter -import java.util.* -import javax.tools.* - -class PipelineCompiler( - private val sourcesInputPath: File, private val sourceFiles: List, - private val resInputPath: File? = null, private val resFiles: List? = null -): DiagnosticListener { - - companion object { - val IS_USABLE by lazy { - val usable = COMPILER != null - - // Send a warning message to console - // will only be sent once (that's why it's done here) - if(!usable) { - Log.warn(TAG, "Unable to compile Java source code in this JVM (the ToolProvider wasn't able to provide a compiler)") - Log.warn(TAG, "For the user, this probably means that the sim is running in a JRE which doesn't include the javac compiler executable") - Log.warn(TAG, "To be able to compile pipelines on runtime, make sure the sim is running on a JDK that includes the javac executable (any JDK probably does)") - } - - usable - } - - val COMPILER = ToolProvider.getSystemJavaCompiler() - - val INDENT = " " - val TAG = "PipelineCompiler" - } - - private var diagnosticBuilders = mutableMapOf() - - val latestDiagnostic: String - get() { - val diagnostic = StringBuilder() - for((_, builder) in diagnosticBuilders) { - diagnostic.appendLine(builder) - diagnostic.appendLine("") - } - - return diagnostic.toString().trim() - } - - val args = arrayListOf( - "-source", "1.8", - "-target", "1.8", - "-g", - "-encoding", "UTF-8", - "-Xlint:unchecked", - "-Xlint:deprecation", - "-XDuseUnsharedTable=true" - ) - - constructor(inputPath: File) : this(inputPath, SysUtil.filesUnder(inputPath, ".java")) - - fun compile(outputJar: File): PipelineCompileResult { - val javac = COMPILER - - val fileManager = PipelineStandardFileManager(javac.getStandardFileManager(this, null, null)) - fileManager.sourcePath = Collections.singleton(sourcesInputPath) - - val javaFileObjects = fileManager.getJavaFileObjects(*sourceFiles.toTypedArray()) - - if(javaFileObjects.iterator().hasNext()) { - SysUtil.deleteFilesUnder(CompiledPipelineManager.CLASSES_OUTPUT_FOLDER) - - val task = javac.getTask( - PrintWriter(System.out), - fileManager, - this, - args, - null, - javaFileObjects - ) - - if(task.call()) { - val outputClasses = fileManager.getLocation(StandardLocation.CLASS_OUTPUT).iterator().next() - - if(resInputPath == null || resFiles == null) { - JarPacker.packClassesUnder(outputJar, outputClasses) - } else { - JarPacker.packResAndClassesUnder(outputJar, outputClasses, resInputPath, resFiles) - } - - return PipelineCompileResult(PipelineCompileStatus.SUCCESS, latestDiagnostic) - } - - return PipelineCompileResult(PipelineCompileStatus.FAILED, latestDiagnostic) - } else { - return PipelineCompileResult(PipelineCompileStatus.NO_SOURCE, "No source files") - } - } - - override fun report(diagnostic: Diagnostic) { - val locale = Locale.getDefault() - val relativeFile = SysUtil.getRelativePath(sourcesInputPath, File(diagnostic.source.name)) - - val builder = diagnosticBuilders[relativeFile.path] ?: StringBuilder() - - if(!diagnosticBuilders.containsKey(relativeFile.path)) { - builder.appendLine("> ${relativeFile.path}") - diagnosticBuilders[relativeFile.path] = builder - } - - val formattedMessage = diagnostic.getMessage(locale).replace("\n", "\n$INDENT") - - builder.appendLine(String.format(locale, "$INDENT(%d:%d): %s: %s", - diagnostic.lineNumber, diagnostic.columnNumber, diagnostic.kind, formattedMessage - )) - } - -} - -enum class PipelineCompileStatus { - SUCCESS, - FAILED, - NO_SOURCE -} - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.pipeline.compiler + +import com.github.serivesmejia.eocvsim.util.Log +import com.github.serivesmejia.eocvsim.util.SysUtil +import com.github.serivesmejia.eocvsim.util.compiler.JarPacker +import java.io.File +import java.io.PrintWriter +import java.util.* +import javax.tools.* + +class PipelineCompiler( + private val sourcesInputPath: File, private val sourceFiles: List, + private val resInputPath: File? = null, private val resFiles: List? = null +): DiagnosticListener { + + companion object { + val IS_USABLE by lazy { + val usable = COMPILER != null + + // Send a warning message to console + // will only be sent once (that's why it's done here) + if(!usable) { + Log.warn(TAG, "Unable to compile Java source code in this JVM (the ToolProvider wasn't able to provide a compiler)") + Log.warn(TAG, "For the user, this probably means that the sim is running in a JRE which doesn't include the javac compiler executable") + Log.warn(TAG, "To be able to compile pipelines on runtime, make sure the sim is running on a JDK that includes the javac executable (any JDK probably does)") + } + + usable + } + + val COMPILER = ToolProvider.getSystemJavaCompiler() + + val INDENT = " " + val TAG = "PipelineCompiler" + } + + private var diagnosticBuilders = mutableMapOf() + + val latestDiagnostic: String + get() { + val diagnostic = StringBuilder() + for((_, builder) in diagnosticBuilders) { + diagnostic.appendLine(builder) + diagnostic.appendLine("") + } + + return diagnostic.toString().trim() + } + + val args = arrayListOf( + "-source", "1.8", + "-target", "1.8", + "-g", + "-encoding", "UTF-8", + "-Xlint:unchecked", + "-Xlint:deprecation", + "-XDuseUnsharedTable=true" + ) + + constructor(inputPath: File) : this(inputPath, SysUtil.filesUnder(inputPath, ".java")) + + fun compile(outputJar: File): PipelineCompileResult { + val javac = COMPILER + + val fileManager = PipelineStandardFileManager(javac.getStandardFileManager(this, null, null)) + fileManager.sourcePath = Collections.singleton(sourcesInputPath) + + val javaFileObjects = fileManager.getJavaFileObjects(*sourceFiles.toTypedArray()) + + if(javaFileObjects.iterator().hasNext()) { + SysUtil.deleteFilesUnder(CompiledPipelineManager.CLASSES_OUTPUT_FOLDER) + + val task = javac.getTask( + PrintWriter(System.out), + fileManager, + this, + args, + null, + javaFileObjects + ) + + if(task.call()) { + val outputClasses = fileManager.getLocation(StandardLocation.CLASS_OUTPUT).iterator().next() + + if(resInputPath == null || resFiles == null) { + JarPacker.packClassesUnder(outputJar, outputClasses) + } else { + JarPacker.packResAndClassesUnder(outputJar, outputClasses, resInputPath, resFiles) + } + + return PipelineCompileResult(PipelineCompileStatus.SUCCESS, latestDiagnostic) + } + + return PipelineCompileResult(PipelineCompileStatus.FAILED, latestDiagnostic) + } else { + return PipelineCompileResult(PipelineCompileStatus.NO_SOURCE, "No source files") + } + } + + override fun report(diagnostic: Diagnostic) { + val locale = Locale.getDefault() + val relativeFile = SysUtil.getRelativePath(sourcesInputPath, File(diagnostic.source.name)) + + val builder = diagnosticBuilders[relativeFile.path] ?: StringBuilder() + + if(!diagnosticBuilders.containsKey(relativeFile.path)) { + builder.appendLine("> ${relativeFile.path}") + diagnosticBuilders[relativeFile.path] = builder + } + + val formattedMessage = diagnostic.getMessage(locale).replace("\n", "\n$INDENT") + + builder.appendLine(String.format(locale, "$INDENT(%d:%d): %s: %s", + diagnostic.lineNumber, diagnostic.columnNumber, diagnostic.kind, formattedMessage + )) + } + +} + +enum class PipelineCompileStatus { + SUCCESS, + FAILED, + NO_SOURCE +} + data class PipelineCompileResult(val status: PipelineCompileStatus, val message: String) \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineStandardFileManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineStandardFileManager.kt index 8b1efc94..868d372b 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineStandardFileManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/compiler/PipelineStandardFileManager.kt @@ -1,67 +1,67 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.pipeline.compiler - -import com.github.serivesmejia.eocvsim.util.Log -import com.github.serivesmejia.eocvsim.util.SysUtil -import com.github.serivesmejia.eocvsim.util.compiler.DelegatingStandardFileManager -import java.io.File -import java.util.* -import javax.tools.StandardJavaFileManager -import javax.tools.StandardLocation - -class PipelineStandardFileManager(delegate: StandardJavaFileManager) : DelegatingStandardFileManager(delegate) { - - var sourcePath: Iterable - set(value) = delegate.setLocation(StandardLocation.SOURCE_PATH, value) - get() = delegate.getLocation(StandardLocation.SOURCE_PATH) - - companion object { - val classpath by lazy { - val classpathList = arrayListOf() - - Log.info(TAG, "Scanning classpath files...") - - for(file in SysUtil.getClasspathFiles()) { - val files = SysUtil.filesUnder(file, ".jar") - files.forEach { Log.info(TAG, "Found classpath file ${it.absolutePath} in classpath") } - - classpathList.addAll(files) - } - - Log.blank() - - classpathList.toList() - } - - private const val TAG = "PipelineStandardFileManager" - } - - init { - delegate.setLocation(StandardLocation.CLASS_PATH, classpath) - delegate.setLocation(StandardLocation.CLASS_OUTPUT, Collections.singletonList(CompiledPipelineManager.CLASSES_OUTPUT_FOLDER)) - delegate.setLocation(StandardLocation.SOURCE_OUTPUT, Collections.singletonList(CompiledPipelineManager.SOURCES_OUTPUT_FOLDER)) - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.pipeline.compiler + +import com.github.serivesmejia.eocvsim.util.Log +import com.github.serivesmejia.eocvsim.util.SysUtil +import com.github.serivesmejia.eocvsim.util.compiler.DelegatingStandardFileManager +import java.io.File +import java.util.* +import javax.tools.StandardJavaFileManager +import javax.tools.StandardLocation + +class PipelineStandardFileManager(delegate: StandardJavaFileManager) : DelegatingStandardFileManager(delegate) { + + var sourcePath: Iterable + set(value) = delegate.setLocation(StandardLocation.SOURCE_PATH, value) + get() = delegate.getLocation(StandardLocation.SOURCE_PATH) + + companion object { + val classpath by lazy { + val classpathList = arrayListOf() + + Log.info(TAG, "Scanning classpath files...") + + for(file in SysUtil.getClasspathFiles()) { + val files = SysUtil.filesUnder(file, ".jar") + files.forEach { Log.info(TAG, "Found classpath file ${it.absolutePath} in classpath") } + + classpathList.addAll(files) + } + + Log.blank() + + classpathList.toList() + } + + private const val TAG = "PipelineStandardFileManager" + } + + init { + delegate.setLocation(StandardLocation.CLASS_PATH, classpath) + delegate.setLocation(StandardLocation.CLASS_OUTPUT, Collections.singletonList(CompiledPipelineManager.CLASSES_OUTPUT_FOLDER)) + delegate.setLocation(StandardLocation.SOURCE_OUTPUT, Collections.singletonList(CompiledPipelineManager.SOURCES_OUTPUT_FOLDER)) + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineExceptionTracker.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineExceptionTracker.kt index c763c03f..b844e203 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineExceptionTracker.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineExceptionTracker.kt @@ -1,171 +1,171 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ -package com.github.serivesmejia.eocvsim.pipeline.util; - -import com.github.serivesmejia.eocvsim.pipeline.PipelineData -import com.github.serivesmejia.eocvsim.pipeline.PipelineManager -import com.github.serivesmejia.eocvsim.util.event.EventHandler -import com.github.serivesmejia.eocvsim.util.StrUtil -import com.github.serivesmejia.eocvsim.util.Log - -class PipelineExceptionTracker(private val pipelineManager: PipelineManager) { - - companion object { - private const val TAG = "PipelineExceptionTracker" - - const val millisExceptionExpire = 25000L - const val cutStacktraceLines = 9 - } - - var currentPipeline: PipelineData? = null - private set - - val exceptionsThrown = mutableMapOf() - val messages = mutableMapOf() - - val onPipelineException = EventHandler("OnPipelineException") - val onNewPipelineException = EventHandler("OnNewPipelineException") - val onPipelineExceptionClear = EventHandler("OnPipelineExceptionClear") - - val onUpdate = EventHandler("OnPipelineExceptionTrackerUpdate") - - fun update(data: PipelineData, ex: Throwable?) { - if(currentPipeline != data) { - exceptionsThrown.clear() - currentPipeline = data - } - - val exStr = if(ex != null) StrUtil.fromException(ex) else "" - - if(ex != null) { - onPipelineException.run() - - val exception = exceptionsThrown.values.stream().filter { - it.stacktrace == exStr - }.findFirst() - - if(!exception.isPresent) { - Log.blank() - Log.warn( - TAG, "Uncaught exception thrown while processing pipeline ${data.clazz.simpleName}", - ex - ) - - Log.warn(TAG, "Note that to avoid spam, continuously equal thrown exceptions are only logged once.") - Log.warn(TAG, "It will be reported once the pipeline stops throwing the exception after $millisExceptionExpire ms") - Log.blank() - - exceptionsThrown[ex] = PipelineException( - 0, exStr, System.currentTimeMillis() - ) - - onNewPipelineException.run() - } - } - - for((e, d) in exceptionsThrown.entries.toTypedArray()) { - if(ex != null && d.stacktrace == exStr) { - d.count++ - d.millisThrown = System.currentTimeMillis() - } - - val timeElapsed = System.currentTimeMillis() - d.millisThrown - if(timeElapsed >= millisExceptionExpire) { - exceptionsThrown.remove(e) - Log.info( - TAG, - "Pipeline ${currentPipeline!!.clazz.simpleName} stopped throwing $e" - ) - - if(exceptionsThrown.isEmpty()) - onPipelineExceptionClear.run() - } - } - - for((message, millisAdded) in messages.entries.toTypedArray()) { - val timeElapsed = System.currentTimeMillis() - millisAdded - if(timeElapsed >= millisExceptionExpire) { - messages.remove(message) - } - } - - onUpdate.run() - } - - val message: String get() { - if(currentPipeline == null) - return "**No pipeline selected**" - - val messageBuilder = StringBuilder() - val pipelineName = currentPipeline!!.clazz.simpleName - - if(exceptionsThrown.isNotEmpty()) { - messageBuilder - .append("**Pipeline $pipelineName is throwing ${exceptionsThrown.size} exception(s)**") - .appendLine("\n") - } else { - messageBuilder.append("**Pipeline $pipelineName ") - - if(pipelineManager.paused) { - messageBuilder.append("is paused (last time was running at ${pipelineManager.pipelineFpsCounter.fps} FPS)") - } else { - messageBuilder.append("running OK at ${pipelineManager.pipelineFpsCounter.fps} FPS") - } - - messageBuilder.append("**").appendLine("\n") - } - - for((_, data) in exceptionsThrown) { - val expiresIn = millisExceptionExpire - (System.currentTimeMillis() - data.millisThrown) - val expiresInSecs = String.format("%.1f", expiresIn.toDouble() / 1000.0) - - val shortStacktrace = StrUtil.cutStringBy( - data.stacktrace, "\n", cutStacktraceLines - ).trim() - - messageBuilder - .appendLine("> $shortStacktrace") - .appendLine() - .appendLine("! It has been thrown ${data.count} times, and will expire in $expiresInSecs seconds !") - .appendLine() - } - - for((message, _) in messages) { - messageBuilder.appendLine(message) - } - - return messageBuilder.toString().trim() - } - - fun clear() = exceptionsThrown.clear() - - fun addMessage(s: String) { - messages[s] = System.currentTimeMillis() - onNewPipelineException.run() - } - - data class PipelineException(var count: Int, - val stacktrace: String, - var millisThrown: Long) - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +package com.github.serivesmejia.eocvsim.pipeline.util; + +import com.github.serivesmejia.eocvsim.pipeline.PipelineData +import com.github.serivesmejia.eocvsim.pipeline.PipelineManager +import com.github.serivesmejia.eocvsim.util.event.EventHandler +import com.github.serivesmejia.eocvsim.util.StrUtil +import com.github.serivesmejia.eocvsim.util.Log + +class PipelineExceptionTracker(private val pipelineManager: PipelineManager) { + + companion object { + private const val TAG = "PipelineExceptionTracker" + + const val millisExceptionExpire = 25000L + const val cutStacktraceLines = 9 + } + + var currentPipeline: PipelineData? = null + private set + + val exceptionsThrown = mutableMapOf() + val messages = mutableMapOf() + + val onPipelineException = EventHandler("OnPipelineException") + val onNewPipelineException = EventHandler("OnNewPipelineException") + val onPipelineExceptionClear = EventHandler("OnPipelineExceptionClear") + + val onUpdate = EventHandler("OnPipelineExceptionTrackerUpdate") + + fun update(data: PipelineData, ex: Throwable?) { + if(currentPipeline != data) { + exceptionsThrown.clear() + currentPipeline = data + } + + val exStr = if(ex != null) StrUtil.fromException(ex) else "" + + if(ex != null) { + onPipelineException.run() + + val exception = exceptionsThrown.values.stream().filter { + it.stacktrace == exStr + }.findFirst() + + if(!exception.isPresent) { + Log.blank() + Log.warn( + TAG, "Uncaught exception thrown while processing pipeline ${data.clazz.simpleName}", + ex + ) + + Log.warn(TAG, "Note that to avoid spam, continuously equal thrown exceptions are only logged once.") + Log.warn(TAG, "It will be reported once the pipeline stops throwing the exception after $millisExceptionExpire ms") + Log.blank() + + exceptionsThrown[ex] = PipelineException( + 0, exStr, System.currentTimeMillis() + ) + + onNewPipelineException.run() + } + } + + for((e, d) in exceptionsThrown.entries.toTypedArray()) { + if(ex != null && d.stacktrace == exStr) { + d.count++ + d.millisThrown = System.currentTimeMillis() + } + + val timeElapsed = System.currentTimeMillis() - d.millisThrown + if(timeElapsed >= millisExceptionExpire) { + exceptionsThrown.remove(e) + Log.info( + TAG, + "Pipeline ${currentPipeline!!.clazz.simpleName} stopped throwing $e" + ) + + if(exceptionsThrown.isEmpty()) + onPipelineExceptionClear.run() + } + } + + for((message, millisAdded) in messages.entries.toTypedArray()) { + val timeElapsed = System.currentTimeMillis() - millisAdded + if(timeElapsed >= millisExceptionExpire) { + messages.remove(message) + } + } + + onUpdate.run() + } + + val message: String get() { + if(currentPipeline == null) + return "**No pipeline selected**" + + val messageBuilder = StringBuilder() + val pipelineName = currentPipeline!!.clazz.simpleName + + if(exceptionsThrown.isNotEmpty()) { + messageBuilder + .append("**Pipeline $pipelineName is throwing ${exceptionsThrown.size} exception(s)**") + .appendLine("\n") + } else { + messageBuilder.append("**Pipeline $pipelineName ") + + if(pipelineManager.paused) { + messageBuilder.append("is paused (last time was running at ${pipelineManager.pipelineFpsCounter.fps} FPS)") + } else { + messageBuilder.append("running OK at ${pipelineManager.pipelineFpsCounter.fps} FPS") + } + + messageBuilder.append("**").appendLine("\n") + } + + for((_, data) in exceptionsThrown) { + val expiresIn = millisExceptionExpire - (System.currentTimeMillis() - data.millisThrown) + val expiresInSecs = String.format("%.1f", expiresIn.toDouble() / 1000.0) + + val shortStacktrace = StrUtil.cutStringBy( + data.stacktrace, "\n", cutStacktraceLines + ).trim() + + messageBuilder + .appendLine("> $shortStacktrace") + .appendLine() + .appendLine("! It has been thrown ${data.count} times, and will expire in $expiresInSecs seconds !") + .appendLine() + } + + for((message, _) in messages) { + messageBuilder.appendLine(message) + } + + return messageBuilder.toString().trim() + } + + fun clear() = exceptionsThrown.clear() + + fun addMessage(s: String) { + messages[s] = System.currentTimeMillis() + onNewPipelineException.run() + } + + data class PipelineException(var count: Int, + val stacktrace: String, + var millisThrown: Long) + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineSnapshot.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineSnapshot.kt index c37810fe..1df10189 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineSnapshot.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/util/PipelineSnapshot.kt @@ -1,132 +1,132 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.pipeline.util - -import com.github.serivesmejia.eocvsim.util.Log -import org.openftc.easyopencv.OpenCvPipeline -import java.lang.reflect.Field -import java.lang.reflect.Modifier -import java.util.* - -class PipelineSnapshot(holdingPipeline: OpenCvPipeline) { - - companion object { - private val TAG = "PipelineSnapshot" - } - - val holdingPipelineName = holdingPipeline::class.simpleName - - val pipelineFieldValues: Map - val pipelineClass = holdingPipeline::class.java - - init { - val fieldValues = mutableMapOf() - - for(field in pipelineClass.declaredFields) { - if(Modifier.isFinal(field.modifiers) || !Modifier.isPublic(field.modifiers)) - continue - - fieldValues[field] = field.get(holdingPipeline) - } - - pipelineFieldValues = fieldValues.toMap() - - Log.info(TAG, "Taken snapshot of pipeline ${pipelineClass.name}") - } - - fun transferTo(otherPipeline: OpenCvPipeline, - lastInitialPipelineSnapshot: PipelineSnapshot? = null) { - if(pipelineClass.name != otherPipeline::class.java.name) return - - val changedList = if(lastInitialPipelineSnapshot != null) - getChangedFieldsComparedTo(PipelineSnapshot(otherPipeline), lastInitialPipelineSnapshot) - else Collections.emptyList() - - fieldValuesLoop@ - for((field, value) in pipelineFieldValues) { - for(changedField in changedList) { - if(changedField.name == field.name && changedField.type == field.type) { - Log.info( - TAG, - "Skipping field ${field.name} since its value was changed in code, compared to the initial state of the pipeline" - ) - - continue@fieldValuesLoop - } - } - - try { - field.set(otherPipeline, value) - } catch(e: Exception) { - Log.warn( - TAG, - "Failed to set field ${field.name} from snapshot of ${pipelineClass.name}. " + - "Retrying with by name lookup logic..." - ) - - try { - val byNameField = otherPipeline::class.java.getDeclaredField(field.name) - byNameField.set(otherPipeline, value) - } catch(e: Exception) { - Log.warn( - TAG, "Definitely failed to set field ${field.name} from snapshot of ${pipelineClass.name}. " + - "Did the source code change?", e - ) - } - } - } - } - - fun getField(name: String): Pair? { - for((field, value) in pipelineFieldValues) { - if(field.name == name) { - return Pair(field, value) - } - } - - return null - } - - private fun getChangedFieldsComparedTo( - pipelineSnapshotA: PipelineSnapshot, - pipelineSnapshotB: PipelineSnapshot - ): List = pipelineSnapshotA.run { - if(holdingPipelineName != pipelineSnapshotB.holdingPipelineName && pipelineClass != pipelineSnapshotB.pipelineClass) - return Collections.emptyList() - - val changedList = mutableListOf() - - for((field, value) in pipelineFieldValues) { - val (otherField, otherValue) = pipelineSnapshotB.getField(field.name) ?: continue - if (field.type != otherField.type) continue - - if(otherValue != value) { - changedList.add(field) - } - } - - return changedList - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.pipeline.util + +import com.github.serivesmejia.eocvsim.util.Log +import org.openftc.easyopencv.OpenCvPipeline +import java.lang.reflect.Field +import java.lang.reflect.Modifier +import java.util.* + +class PipelineSnapshot(holdingPipeline: OpenCvPipeline) { + + companion object { + private val TAG = "PipelineSnapshot" + } + + val holdingPipelineName = holdingPipeline::class.simpleName + + val pipelineFieldValues: Map + val pipelineClass = holdingPipeline::class.java + + init { + val fieldValues = mutableMapOf() + + for(field in pipelineClass.declaredFields) { + if(Modifier.isFinal(field.modifiers) || !Modifier.isPublic(field.modifiers)) + continue + + fieldValues[field] = field.get(holdingPipeline) + } + + pipelineFieldValues = fieldValues.toMap() + + Log.info(TAG, "Taken snapshot of pipeline ${pipelineClass.name}") + } + + fun transferTo(otherPipeline: OpenCvPipeline, + lastInitialPipelineSnapshot: PipelineSnapshot? = null) { + if(pipelineClass.name != otherPipeline::class.java.name) return + + val changedList = if(lastInitialPipelineSnapshot != null) + getChangedFieldsComparedTo(PipelineSnapshot(otherPipeline), lastInitialPipelineSnapshot) + else Collections.emptyList() + + fieldValuesLoop@ + for((field, value) in pipelineFieldValues) { + for(changedField in changedList) { + if(changedField.name == field.name && changedField.type == field.type) { + Log.info( + TAG, + "Skipping field ${field.name} since its value was changed in code, compared to the initial state of the pipeline" + ) + + continue@fieldValuesLoop + } + } + + try { + field.set(otherPipeline, value) + } catch(e: Exception) { + Log.warn( + TAG, + "Failed to set field ${field.name} from snapshot of ${pipelineClass.name}. " + + "Retrying with by name lookup logic..." + ) + + try { + val byNameField = otherPipeline::class.java.getDeclaredField(field.name) + byNameField.set(otherPipeline, value) + } catch(e: Exception) { + Log.warn( + TAG, "Definitely failed to set field ${field.name} from snapshot of ${pipelineClass.name}. " + + "Did the source code change?", e + ) + } + } + } + } + + fun getField(name: String): Pair? { + for((field, value) in pipelineFieldValues) { + if(field.name == name) { + return Pair(field, value) + } + } + + return null + } + + private fun getChangedFieldsComparedTo( + pipelineSnapshotA: PipelineSnapshot, + pipelineSnapshotB: PipelineSnapshot + ): List = pipelineSnapshotA.run { + if(holdingPipelineName != pipelineSnapshotB.holdingPipelineName && pipelineClass != pipelineSnapshotB.pipelineClass) + return Collections.emptyList() + + val changedList = mutableListOf() + + for((field, value) in pipelineFieldValues) { + val (otherField, otherValue) = pipelineSnapshotB.getField(field.name) ?: continue + if (field.type != otherField.type) continue + + if(otherValue != value) { + changedList.add(field) + } + } + + return changedList + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunableField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunableField.java index 19a7c7f3..dfecbe76 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunableField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunableField.java @@ -1,147 +1,147 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.tuner; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; -import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanelConfig; -import com.github.serivesmejia.eocvsim.util.event.EventHandler; -import org.openftc.easyopencv.OpenCvPipeline; - -import java.lang.reflect.Field; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; - -public abstract class TunableField { - - protected Field reflectionField; - protected TunableFieldPanel fieldPanel; - - protected OpenCvPipeline pipeline; - protected AllowMode allowMode; - protected EOCVSim eocvSim; - - protected Object initialFieldValue; - - private int guiFieldAmount = 1; - private int guiComboBoxAmount = 0; - - public final EventHandler onValueChange = new EventHandler("TunableField-ValueChange"); - - private TunableFieldPanel.Mode recommendedMode = null; - - public TunableField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim, AllowMode allowMode) throws IllegalAccessException { - this.reflectionField = reflectionField; - this.pipeline = instance; - this.allowMode = allowMode; - this.eocvSim = eocvSim; - - initialFieldValue = reflectionField.get(instance); - } - - public TunableField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - this(instance, reflectionField, eocvSim, AllowMode.TEXT); - } - - public abstract void init(); - - public abstract void update(); - - public abstract void updateGuiFieldValues(); - - public void setPipelineFieldValue(T newValue) throws IllegalAccessException { - if (hasChanged()) { //execute if value is not the same to save resources - reflectionField.set(pipeline, newValue); - onValueChange.run(); - } - } - - public abstract void setGuiFieldValue(int index, String newValue) throws IllegalAccessException; - - public void setGuiComboBoxValue(int index, String newValue) throws IllegalAccessException { } - - public final void setTunableFieldPanel(TunableFieldPanel fieldPanel) { - this.fieldPanel = fieldPanel; - } - - protected final void setRecommendedPanelMode(TunableFieldPanel.Mode mode) { - recommendedMode = mode; - } - - public final void evalRecommendedPanelMode() { - TunableFieldPanelConfig configPanel = fieldPanel.panelOptions.getConfigPanel(); - TunableFieldPanelConfig.ConfigSource configSource = configPanel.getLocalConfig().getSource(); - //only apply the recommendation if user hasn't - //configured a global or specific field config - if(recommendedMode != null && fieldPanel != null && configSource == TunableFieldPanelConfig.ConfigSource.GLOBAL_DEFAULT) { - fieldPanel.setMode(recommendedMode); - } - } - - public abstract T getValue(); - - public abstract Object getGuiFieldValue(int index); - - public Object[] getGuiComboBoxValues(int index) { - return new Object[0]; - } - - public final int getGuiFieldAmount() { - return guiFieldAmount; - } - - public final void setGuiFieldAmount(int amount) { - this.guiFieldAmount = amount; - } - - public final int getGuiComboBoxAmount() { - return guiComboBoxAmount; - } - - public final void setGuiComboBoxAmount(int amount) { - this.guiComboBoxAmount = amount; - } - - public final String getFieldName() { - return reflectionField.getName(); - } - - public final AllowMode getAllowMode() { - return allowMode; - } - - public final boolean isOnlyNumbers() { - return getAllowMode() == TunableField.AllowMode.ONLY_NUMBERS || - getAllowMode() == TunableField.AllowMode.ONLY_NUMBERS_DECIMAL; - } - - public abstract boolean hasChanged(); - - public final EOCVSim getEOCVSim() { - return eocvSim; - } - - public enum AllowMode {ONLY_NUMBERS, ONLY_NUMBERS_DECIMAL, TEXT} - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.tuner; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; +import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanelConfig; +import com.github.serivesmejia.eocvsim.util.event.EventHandler; +import org.openftc.easyopencv.OpenCvPipeline; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +public abstract class TunableField { + + protected Field reflectionField; + protected TunableFieldPanel fieldPanel; + + protected OpenCvPipeline pipeline; + protected AllowMode allowMode; + protected EOCVSim eocvSim; + + protected Object initialFieldValue; + + private int guiFieldAmount = 1; + private int guiComboBoxAmount = 0; + + public final EventHandler onValueChange = new EventHandler("TunableField-ValueChange"); + + private TunableFieldPanel.Mode recommendedMode = null; + + public TunableField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim, AllowMode allowMode) throws IllegalAccessException { + this.reflectionField = reflectionField; + this.pipeline = instance; + this.allowMode = allowMode; + this.eocvSim = eocvSim; + + initialFieldValue = reflectionField.get(instance); + } + + public TunableField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + this(instance, reflectionField, eocvSim, AllowMode.TEXT); + } + + public abstract void init(); + + public abstract void update(); + + public abstract void updateGuiFieldValues(); + + public void setPipelineFieldValue(T newValue) throws IllegalAccessException { + if (hasChanged()) { //execute if value is not the same to save resources + reflectionField.set(pipeline, newValue); + onValueChange.run(); + } + } + + public abstract void setGuiFieldValue(int index, String newValue) throws IllegalAccessException; + + public void setGuiComboBoxValue(int index, String newValue) throws IllegalAccessException { } + + public final void setTunableFieldPanel(TunableFieldPanel fieldPanel) { + this.fieldPanel = fieldPanel; + } + + protected final void setRecommendedPanelMode(TunableFieldPanel.Mode mode) { + recommendedMode = mode; + } + + public final void evalRecommendedPanelMode() { + TunableFieldPanelConfig configPanel = fieldPanel.panelOptions.getConfigPanel(); + TunableFieldPanelConfig.ConfigSource configSource = configPanel.getLocalConfig().getSource(); + //only apply the recommendation if user hasn't + //configured a global or specific field config + if(recommendedMode != null && fieldPanel != null && configSource == TunableFieldPanelConfig.ConfigSource.GLOBAL_DEFAULT) { + fieldPanel.setMode(recommendedMode); + } + } + + public abstract T getValue(); + + public abstract Object getGuiFieldValue(int index); + + public Object[] getGuiComboBoxValues(int index) { + return new Object[0]; + } + + public final int getGuiFieldAmount() { + return guiFieldAmount; + } + + public final void setGuiFieldAmount(int amount) { + this.guiFieldAmount = amount; + } + + public final int getGuiComboBoxAmount() { + return guiComboBoxAmount; + } + + public final void setGuiComboBoxAmount(int amount) { + this.guiComboBoxAmount = amount; + } + + public final String getFieldName() { + return reflectionField.getName(); + } + + public final AllowMode getAllowMode() { + return allowMode; + } + + public final boolean isOnlyNumbers() { + return getAllowMode() == TunableField.AllowMode.ONLY_NUMBERS || + getAllowMode() == TunableField.AllowMode.ONLY_NUMBERS_DECIMAL; + } + + public abstract boolean hasChanged(); + + public final EOCVSim getEOCVSim() { + return eocvSim; + } + + public enum AllowMode {ONLY_NUMBERS, ONLY_NUMBERS_DECIMAL, TEXT} + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunableFieldAcceptor.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunableFieldAcceptor.kt index dcfa4a43..787e8b8f 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunableFieldAcceptor.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunableFieldAcceptor.kt @@ -1,28 +1,28 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.tuner - -interface TunableFieldAcceptor { - fun accept(clazz: Class<*>): Boolean +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.tuner + +interface TunableFieldAcceptor { + fun accept(clazz: Class<*>): Boolean } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunableFieldAcceptorManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunableFieldAcceptorManager.kt index 19f47e03..03b9fe97 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunableFieldAcceptorManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunableFieldAcceptorManager.kt @@ -1,28 +1,28 @@ -package com.github.serivesmejia.eocvsim.tuner - -import com.github.serivesmejia.eocvsim.util.Log -import java.util.HashMap - -class TunableFieldAcceptorManager(private val acceptors: HashMap>, Class>) { - - fun accept(clazz: Class<*>): Class>? { - for((fieldClass, acceptorClass) in acceptors) { - //try getting a constructor for this acceptor - val acceptorConstructor = try { - acceptorClass.getConstructor() //get constructor with no params - } catch(ex: NoSuchMethodException) { - Log.warn("TunableFieldAcceptorManager", "TunableFieldAcceptor ${acceptorClass.typeName} doesn't implement a constructor with zero parameters", ex) - continue - } - - val acceptor = acceptorConstructor.newInstance() //create an instance of this acceptor - - if(acceptor.accept(clazz)) { //try accepting the given clazz type - return fieldClass //wooo someone accepted our type! return to tell who did. - } - } - - return null //no one accepted our type... poor clazz :( - } - +package com.github.serivesmejia.eocvsim.tuner + +import com.github.serivesmejia.eocvsim.util.Log +import java.util.HashMap + +class TunableFieldAcceptorManager(private val acceptors: HashMap>, Class>) { + + fun accept(clazz: Class<*>): Class>? { + for((fieldClass, acceptorClass) in acceptors) { + //try getting a constructor for this acceptor + val acceptorConstructor = try { + acceptorClass.getConstructor() //get constructor with no params + } catch(ex: NoSuchMethodException) { + Log.warn("TunableFieldAcceptorManager", "TunableFieldAcceptor ${acceptorClass.typeName} doesn't implement a constructor with zero parameters", ex) + continue + } + + val acceptor = acceptorConstructor.newInstance() //create an instance of this acceptor + + if(acceptor.accept(clazz)) { //try accepting the given clazz type + return fieldClass //wooo someone accepted our type! return to tell who did. + } + } + + return null //no one accepted our type... poor clazz :( + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunerManager.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunerManager.java index d97f235b..1b515f38 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunerManager.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunerManager.java @@ -1,171 +1,171 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.tuner; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; -import com.github.serivesmejia.eocvsim.tuner.scanner.AnnotatedTunableFieldScanner; -import com.github.serivesmejia.eocvsim.util.Log; -import com.github.serivesmejia.eocvsim.util.ReflectUtil; -import org.openftc.easyopencv.OpenCvPipeline; - -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -@SuppressWarnings("rawtypes") -public class TunerManager { - - private final EOCVSim eocvSim; - - private final List fields = new ArrayList<>(); - - private TunableFieldAcceptorManager acceptorManager = null; - - private static HashMap>> tunableFieldsTypes = null; - private static HashMap>, Class> tunableFieldAcceptors = null; - - private boolean firstInit = true; - - public TunerManager(EOCVSim eocvSim) { - this.eocvSim = eocvSim; - } - - public void init() { - if(tunableFieldsTypes == null) { - AnnotatedTunableFieldScanner.ScanResult result = new AnnotatedTunableFieldScanner( - eocvSim.getParams().getScanForTunableFieldsIn() - ).scan(); - - tunableFieldsTypes = result.getTunableFields(); - tunableFieldAcceptors = result.getAcceptors(); - } - - // for some reason, acceptorManager becomes null after a certain time passes - // (maybe garbage collected? i don't know for sure...), but we can simply recover - // from this by creating a new one with the found acceptors by the scanner, no problem. - if(acceptorManager == null) - acceptorManager = new TunableFieldAcceptorManager(tunableFieldAcceptors); - - if (firstInit) { - eocvSim.pipelineManager.onPipelineChange.doPersistent(this::reset); - firstInit = false; - } - - if (eocvSim.pipelineManager.getCurrentPipeline() != null) { - addFieldsFrom(eocvSim.pipelineManager.getCurrentPipeline()); - eocvSim.visualizer.updateTunerFields(createTunableFieldPanels()); - - for(TunableField field : fields) { - field.init(); - } - } - } - - public void update() { - //update all fields - for(TunableField field : fields.toArray(new TunableField[0])) { - try { - field.update(); - } catch(Exception ex) { - Log.error("Error while updating field " + field.getFieldName(), ex); - } - - //check if this field has requested to reevaluate config for all panels - if(field.fieldPanel.hasRequestedAllConfigReeval()) { - //if so, iterate through all fields to reevaluate - for(TunableField f : fields.toArray(new TunableField[0])) { - f.fieldPanel.panelOptions.reevaluateConfig(); - } - } - } - } - - public void reset() { - fields.clear(); - init(); - } - - public void addFieldsFrom(OpenCvPipeline pipeline) { - - if (pipeline == null) return; - - Field[] fields = pipeline.getClass().getFields(); - - for (Field field : fields) { - - //we only accept non-final fields - if (Modifier.isFinal(field.getModifiers())) continue; - - Class type = field.getType(); - if (field.getType().isPrimitive()) { //wrap to java object equivalent if field type is primitive - type = ReflectUtil.wrap(type); - } - - Class tunableFieldClass = null; - - if(tunableFieldsTypes.containsKey(type)) { - tunableFieldClass = tunableFieldsTypes.get(type); - } else { - //if we don't have a class yet, use our acceptors - if(acceptorManager != null) tunableFieldClass = acceptorManager.accept(type); - //still haven't got anything, give up here. - if(tunableFieldClass == null) continue; - } - - //yay we have a registered TunableField which handles this - //now, lets do some more reflection to instantiate this TunableField - //and add it to the list... - try { - Constructor constructor = tunableFieldClass.getConstructor(OpenCvPipeline.class, Field.class, EOCVSim.class); - this.fields.add(constructor.newInstance(pipeline, field, eocvSim)); - } catch (Exception ex) { - //oops rip - Log.error("TunerManager", "Reflection error while processing field: " + field.getName(), ex); - } - - } - } - - public void reevaluateConfigs() { - for(TunableField field : fields) { - field.fieldPanel.panelOptions.reevaluateConfig(); - } - } - - private List createTunableFieldPanels() { - List panels = new ArrayList<>(); - - for (TunableField field : fields) { - panels.add(new TunableFieldPanel(field, eocvSim)); - } - - return panels; - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.tuner; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; +import com.github.serivesmejia.eocvsim.tuner.scanner.AnnotatedTunableFieldScanner; +import com.github.serivesmejia.eocvsim.util.Log; +import com.github.serivesmejia.eocvsim.util.ReflectUtil; +import org.openftc.easyopencv.OpenCvPipeline; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +@SuppressWarnings("rawtypes") +public class TunerManager { + + private final EOCVSim eocvSim; + + private final List fields = new ArrayList<>(); + + private TunableFieldAcceptorManager acceptorManager = null; + + private static HashMap>> tunableFieldsTypes = null; + private static HashMap>, Class> tunableFieldAcceptors = null; + + private boolean firstInit = true; + + public TunerManager(EOCVSim eocvSim) { + this.eocvSim = eocvSim; + } + + public void init() { + if(tunableFieldsTypes == null) { + AnnotatedTunableFieldScanner.ScanResult result = new AnnotatedTunableFieldScanner( + eocvSim.getParams().getScanForTunableFieldsIn() + ).scan(); + + tunableFieldsTypes = result.getTunableFields(); + tunableFieldAcceptors = result.getAcceptors(); + } + + // for some reason, acceptorManager becomes null after a certain time passes + // (maybe garbage collected? i don't know for sure...), but we can simply recover + // from this by creating a new one with the found acceptors by the scanner, no problem. + if(acceptorManager == null) + acceptorManager = new TunableFieldAcceptorManager(tunableFieldAcceptors); + + if (firstInit) { + eocvSim.pipelineManager.onPipelineChange.doPersistent(this::reset); + firstInit = false; + } + + if (eocvSim.pipelineManager.getCurrentPipeline() != null) { + addFieldsFrom(eocvSim.pipelineManager.getCurrentPipeline()); + eocvSim.visualizer.updateTunerFields(createTunableFieldPanels()); + + for(TunableField field : fields) { + field.init(); + } + } + } + + public void update() { + //update all fields + for(TunableField field : fields.toArray(new TunableField[0])) { + try { + field.update(); + } catch(Exception ex) { + Log.error("Error while updating field " + field.getFieldName(), ex); + } + + //check if this field has requested to reevaluate config for all panels + if(field.fieldPanel.hasRequestedAllConfigReeval()) { + //if so, iterate through all fields to reevaluate + for(TunableField f : fields.toArray(new TunableField[0])) { + f.fieldPanel.panelOptions.reevaluateConfig(); + } + } + } + } + + public void reset() { + fields.clear(); + init(); + } + + public void addFieldsFrom(OpenCvPipeline pipeline) { + + if (pipeline == null) return; + + Field[] fields = pipeline.getClass().getFields(); + + for (Field field : fields) { + + //we only accept non-final fields + if (Modifier.isFinal(field.getModifiers())) continue; + + Class type = field.getType(); + if (field.getType().isPrimitive()) { //wrap to java object equivalent if field type is primitive + type = ReflectUtil.wrap(type); + } + + Class tunableFieldClass = null; + + if(tunableFieldsTypes.containsKey(type)) { + tunableFieldClass = tunableFieldsTypes.get(type); + } else { + //if we don't have a class yet, use our acceptors + if(acceptorManager != null) tunableFieldClass = acceptorManager.accept(type); + //still haven't got anything, give up here. + if(tunableFieldClass == null) continue; + } + + //yay we have a registered TunableField which handles this + //now, lets do some more reflection to instantiate this TunableField + //and add it to the list... + try { + Constructor constructor = tunableFieldClass.getConstructor(OpenCvPipeline.class, Field.class, EOCVSim.class); + this.fields.add(constructor.newInstance(pipeline, field, eocvSim)); + } catch (Exception ex) { + //oops rip + Log.error("TunerManager", "Reflection error while processing field: " + field.getName(), ex); + } + + } + } + + public void reevaluateConfigs() { + for(TunableField field : fields) { + field.fieldPanel.panelOptions.reevaluateConfig(); + } + } + + private List createTunableFieldPanels() { + List panels = new ArrayList<>(); + + for (TunableField field : fields) { + panels.add(new TunableFieldPanel(field, eocvSim)); + } + + return panels; + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/BooleanField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/BooleanField.java index 687cfba4..289e49a3 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/BooleanField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/BooleanField.java @@ -1,106 +1,106 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.tuner.field; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.tuner.TunableField; -import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; -import org.openftc.easyopencv.OpenCvPipeline; - -import java.lang.reflect.Field; - -@RegisterTunableField -public class BooleanField extends TunableField { - - boolean value; - - boolean lastVal; - volatile boolean hasChanged = false; - - public BooleanField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - - super(instance, reflectionField, eocvSim, AllowMode.TEXT); - - setGuiFieldAmount(0); - setGuiComboBoxAmount(1); - - value = (boolean) initialFieldValue; - - } - - @Override - public void init() {} - - @Override - public void update() { - - hasChanged = value != lastVal; - - if (hasChanged) { //update values in GUI if they changed since last check - updateGuiFieldValues(); - } - - lastVal = value; - - } - - @Override - public void updateGuiFieldValues() { - fieldPanel.setComboBoxSelection(0, value); - } - - @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { - setGuiComboBoxValue(index, newValue); - } - - @Override - public void setGuiComboBoxValue(int index, String newValue) throws IllegalAccessException { - value = Boolean.parseBoolean(newValue); - setPipelineFieldValue(value); - lastVal = value; - } - - @Override - public Boolean getValue() { - return value; - } - - @Override - public Object getGuiFieldValue(int index) { - return value; - } - - @Override - public Object[] getGuiComboBoxValues(int index) { - return new Boolean[]{value, !value}; - } - - @Override - public boolean hasChanged() { - hasChanged = value != lastVal; - return hasChanged; - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.tuner.field; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.tuner.TunableField; +import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; +import org.openftc.easyopencv.OpenCvPipeline; + +import java.lang.reflect.Field; + +@RegisterTunableField +public class BooleanField extends TunableField { + + boolean value; + + boolean lastVal; + volatile boolean hasChanged = false; + + public BooleanField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + + super(instance, reflectionField, eocvSim, AllowMode.TEXT); + + setGuiFieldAmount(0); + setGuiComboBoxAmount(1); + + value = (boolean) initialFieldValue; + + } + + @Override + public void init() {} + + @Override + public void update() { + + hasChanged = value != lastVal; + + if (hasChanged) { //update values in GUI if they changed since last check + updateGuiFieldValues(); + } + + lastVal = value; + + } + + @Override + public void updateGuiFieldValues() { + fieldPanel.setComboBoxSelection(0, value); + } + + @Override + public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { + setGuiComboBoxValue(index, newValue); + } + + @Override + public void setGuiComboBoxValue(int index, String newValue) throws IllegalAccessException { + value = Boolean.parseBoolean(newValue); + setPipelineFieldValue(value); + lastVal = value; + } + + @Override + public Boolean getValue() { + return value; + } + + @Override + public Object getGuiFieldValue(int index) { + return value; + } + + @Override + public Object[] getGuiComboBoxValues(int index) { + return new Boolean[]{value, !value}; + } + + @Override + public boolean hasChanged() { + hasChanged = value != lastVal; + return hasChanged; + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/EnumField.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/EnumField.kt index 073ee51a..42f60e99 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/EnumField.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/EnumField.kt @@ -1,65 +1,65 @@ -package com.github.serivesmejia.eocvsim.tuner.field - -import com.github.serivesmejia.eocvsim.EOCVSim -import com.github.serivesmejia.eocvsim.tuner.TunableField -import com.github.serivesmejia.eocvsim.tuner.TunableFieldAcceptor -import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField -import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableFieldAcceptor -import org.openftc.easyopencv.OpenCvPipeline -import java.lang.reflect.Field - -@RegisterTunableField -class EnumField(private val instance: OpenCvPipeline, - reflectionField: Field, - eocvSim: EOCVSim) : TunableField>(instance, reflectionField, eocvSim, AllowMode.TEXT) { - - val values = reflectionField.type.enumConstants - - private val initialValue = initialFieldValue as Enum<*> - - private var currentValue = initialValue - private var beforeValue: Any? = null - - init { - guiComboBoxAmount = 1 - guiFieldAmount = 0 - } - - override fun init() { - fieldPanel.setComboBoxSelection(0, currentValue) - } - - override fun update() { - if(hasChanged()) { - currentValue = value - updateGuiFieldValues() - } - beforeValue = currentValue - } - - override fun updateGuiFieldValues() { - fieldPanel.setComboBoxSelection(0, currentValue) - } - - override fun setGuiComboBoxValue(index: Int, newValue: String) = setGuiFieldValue(index, newValue) - - override fun setGuiFieldValue(index: Int, newValue: String) { - currentValue = java.lang.Enum.valueOf(initialValue::class.java, newValue) - reflectionField.set(instance, currentValue) - } - - override fun getValue() = currentValue - - override fun getGuiFieldValue(index: Int) = currentValue.name - - override fun getGuiComboBoxValues(index: Int): Array { - return values - } - - override fun hasChanged() = reflectionField.get(instance) != beforeValue - - class EnumFieldAcceptor : TunableFieldAcceptor { - override fun accept(clazz: Class<*>) = clazz.isEnum - } - +package com.github.serivesmejia.eocvsim.tuner.field + +import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.tuner.TunableField +import com.github.serivesmejia.eocvsim.tuner.TunableFieldAcceptor +import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField +import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableFieldAcceptor +import org.openftc.easyopencv.OpenCvPipeline +import java.lang.reflect.Field + +@RegisterTunableField +class EnumField(private val instance: OpenCvPipeline, + reflectionField: Field, + eocvSim: EOCVSim) : TunableField>(instance, reflectionField, eocvSim, AllowMode.TEXT) { + + val values = reflectionField.type.enumConstants + + private val initialValue = initialFieldValue as Enum<*> + + private var currentValue = initialValue + private var beforeValue: Any? = null + + init { + guiComboBoxAmount = 1 + guiFieldAmount = 0 + } + + override fun init() { + fieldPanel.setComboBoxSelection(0, currentValue) + } + + override fun update() { + if(hasChanged()) { + currentValue = value + updateGuiFieldValues() + } + beforeValue = currentValue + } + + override fun updateGuiFieldValues() { + fieldPanel.setComboBoxSelection(0, currentValue) + } + + override fun setGuiComboBoxValue(index: Int, newValue: String) = setGuiFieldValue(index, newValue) + + override fun setGuiFieldValue(index: Int, newValue: String) { + currentValue = java.lang.Enum.valueOf(initialValue::class.java, newValue) + reflectionField.set(instance, currentValue) + } + + override fun getValue() = currentValue + + override fun getGuiFieldValue(index: Int) = currentValue.name + + override fun getGuiComboBoxValues(index: Int): Array { + return values + } + + override fun hasChanged() = reflectionField.get(instance) != beforeValue + + class EnumFieldAcceptor : TunableFieldAcceptor { + override fun accept(clazz: Class<*>) = clazz.isEnum + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/NumericField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/NumericField.java index 29a19690..cdca560a 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/NumericField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/NumericField.java @@ -1,89 +1,89 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.tuner.field; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; -import com.github.serivesmejia.eocvsim.tuner.TunableField; -import org.openftc.easyopencv.OpenCvPipeline; - -import java.lang.reflect.Field; - -public class NumericField extends TunableField { - - protected T value; - - protected volatile boolean hasChanged = false; - - public NumericField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim, AllowMode allowMode) throws IllegalAccessException { - super(instance, reflectionField, eocvSim, allowMode); - } - - @Override - public void init() { - setRecommendedPanelMode(TunableFieldPanel.Mode.TEXTBOXES); - } - - @Override - public void update() { - if (value == null) return; - - try { - value = (T) reflectionField.get(pipeline); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - - hasChanged = hasChanged(); - - if (hasChanged) { - updateGuiFieldValues(); - } - } - - @Override - public void updateGuiFieldValues() { - fieldPanel.setFieldValue(0, value); - } - - @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { - } - - @Override - public T getValue() { - return value; - } - - @Override - public Object getGuiFieldValue(int index) { - return value; - } - - @Override - public boolean hasChanged() { - return false; - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.tuner.field; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; +import com.github.serivesmejia.eocvsim.tuner.TunableField; +import org.openftc.easyopencv.OpenCvPipeline; + +import java.lang.reflect.Field; + +public class NumericField extends TunableField { + + protected T value; + + protected volatile boolean hasChanged = false; + + public NumericField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim, AllowMode allowMode) throws IllegalAccessException { + super(instance, reflectionField, eocvSim, allowMode); + } + + @Override + public void init() { + setRecommendedPanelMode(TunableFieldPanel.Mode.TEXTBOXES); + } + + @Override + public void update() { + if (value == null) return; + + try { + value = (T) reflectionField.get(pipeline); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + + hasChanged = hasChanged(); + + if (hasChanged) { + updateGuiFieldValues(); + } + } + + @Override + public void updateGuiFieldValues() { + fieldPanel.setFieldValue(0, value); + } + + @Override + public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { + } + + @Override + public T getValue() { + return value; + } + + @Override + public Object getGuiFieldValue(int index) { + return value; + } + + @Override + public boolean hasChanged() { + return false; + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/StringField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/StringField.java index af69baad..f1cd85a3 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/StringField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/StringField.java @@ -1,98 +1,98 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.tuner.field; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; -import com.github.serivesmejia.eocvsim.tuner.TunableField; -import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; -import org.openftc.easyopencv.OpenCvPipeline; - -import java.lang.reflect.Field; - -@RegisterTunableField -public class StringField extends TunableField { - - String value; - - String lastVal = ""; - - volatile boolean hasChanged = false; - - public StringField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - - super(instance, reflectionField, eocvSim, AllowMode.TEXT); - value = (String) initialFieldValue; - - } - - @Override - public void init() { - setRecommendedPanelMode(TunableFieldPanel.Mode.TEXTBOXES); - } - - @Override - public void update() { - hasChanged = !value.equals(lastVal); - - if (hasChanged) { //update values in GUI if they changed since last check - updateGuiFieldValues(); - } - - lastVal = value; - } - - @Override - public void updateGuiFieldValues() { - fieldPanel.setFieldValue(0, value); - } - - @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { - - value = newValue; - - setPipelineFieldValue(value); - - lastVal = value; - - } - - @Override - public String getValue() { - return value; - } - - @Override - public Object getGuiFieldValue(int index) { - return value; - } - - @Override - public boolean hasChanged() { - hasChanged = !value.equals(lastVal); - return hasChanged; - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.tuner.field; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; +import com.github.serivesmejia.eocvsim.tuner.TunableField; +import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; +import org.openftc.easyopencv.OpenCvPipeline; + +import java.lang.reflect.Field; + +@RegisterTunableField +public class StringField extends TunableField { + + String value; + + String lastVal = ""; + + volatile boolean hasChanged = false; + + public StringField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + + super(instance, reflectionField, eocvSim, AllowMode.TEXT); + value = (String) initialFieldValue; + + } + + @Override + public void init() { + setRecommendedPanelMode(TunableFieldPanel.Mode.TEXTBOXES); + } + + @Override + public void update() { + hasChanged = !value.equals(lastVal); + + if (hasChanged) { //update values in GUI if they changed since last check + updateGuiFieldValues(); + } + + lastVal = value; + } + + @Override + public void updateGuiFieldValues() { + fieldPanel.setFieldValue(0, value); + } + + @Override + public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { + + value = newValue; + + setPipelineFieldValue(value); + + lastVal = value; + + } + + @Override + public String getValue() { + return value; + } + + @Override + public Object getGuiFieldValue(int index) { + return value; + } + + @Override + public boolean hasChanged() { + hasChanged = !value.equals(lastVal); + return hasChanged; + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/PointField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/PointField.java index 65b4c185..0f545153 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/PointField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/PointField.java @@ -1,113 +1,113 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.tuner.field.cv; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.tuner.TunableField; -import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; -import org.opencv.core.Point; -import org.openftc.easyopencv.OpenCvPipeline; - -import java.lang.reflect.Field; - -@RegisterTunableField -public class PointField extends TunableField { - - Point point; - - double[] lastXY = {0, 0}; - - volatile boolean hasChanged = false; - - public PointField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - - super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); - - Point p = (Point) initialFieldValue; - - point = new Point(p.x, p.y); - - setGuiFieldAmount(2); - - } - - @Override - public void init() { } - - @Override - public void update() { - - hasChanged = point.x != lastXY[0] || point.y != lastXY[1]; - - if (hasChanged) { //update values in GUI if they changed since last check - updateGuiFieldValues(); - } - - lastXY = new double[]{point.x, point.y}; - - } - - @Override - public void updateGuiFieldValues() { - fieldPanel.setFieldValue(0, point.x); - fieldPanel.setFieldValue(1, point.y); - } - - @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { - - try { - double value = Double.parseDouble(newValue); - if (index == 0) { - point.x = value; - } else { - point.y = value; - } - } catch (NumberFormatException ex) { - throw new IllegalArgumentException("Parameter should be a valid numeric String"); - } - - setPipelineFieldValue(point); - - lastXY = new double[]{point.x, point.y}; - - } - - @Override - public Point getValue() { - return point; - } - - @Override - public Object getGuiFieldValue(int index) { - return index == 0 ? point.x : point.y; - } - - @Override - public boolean hasChanged() { - hasChanged = point.x != lastXY[0] || point.y != lastXY[1]; - return hasChanged; - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.tuner.field.cv; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.tuner.TunableField; +import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; +import org.opencv.core.Point; +import org.openftc.easyopencv.OpenCvPipeline; + +import java.lang.reflect.Field; + +@RegisterTunableField +public class PointField extends TunableField { + + Point point; + + double[] lastXY = {0, 0}; + + volatile boolean hasChanged = false; + + public PointField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + + super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); + + Point p = (Point) initialFieldValue; + + point = new Point(p.x, p.y); + + setGuiFieldAmount(2); + + } + + @Override + public void init() { } + + @Override + public void update() { + + hasChanged = point.x != lastXY[0] || point.y != lastXY[1]; + + if (hasChanged) { //update values in GUI if they changed since last check + updateGuiFieldValues(); + } + + lastXY = new double[]{point.x, point.y}; + + } + + @Override + public void updateGuiFieldValues() { + fieldPanel.setFieldValue(0, point.x); + fieldPanel.setFieldValue(1, point.y); + } + + @Override + public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { + + try { + double value = Double.parseDouble(newValue); + if (index == 0) { + point.x = value; + } else { + point.y = value; + } + } catch (NumberFormatException ex) { + throw new IllegalArgumentException("Parameter should be a valid numeric String"); + } + + setPipelineFieldValue(point); + + lastXY = new double[]{point.x, point.y}; + + } + + @Override + public Point getValue() { + return point; + } + + @Override + public Object getGuiFieldValue(int index) { + return index == 0 ? point.x : point.y; + } + + @Override + public boolean hasChanged() { + hasChanged = point.x != lastXY[0] || point.y != lastXY[1]; + return hasChanged; + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/RectField.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/RectField.kt index ed612476..648ea148 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/RectField.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/RectField.kt @@ -1,101 +1,101 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.tuner.field.cv - -import com.github.serivesmejia.eocvsim.EOCVSim -import com.github.serivesmejia.eocvsim.tuner.TunableField -import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField -import org.opencv.core.Rect -import org.openftc.easyopencv.OpenCvPipeline -import java.lang.reflect.Field - -@RegisterTunableField -class RectField(instance: OpenCvPipeline, reflectionField: Field, eocvSim: EOCVSim) : - TunableField(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL) { - - private var rect = arrayOf(0.0, 0.0, 0.0, 0.0) - private var lastRect = arrayOf(0.0, 0.0, 0.0, 0.0) - - @Volatile private var hasChanged = false - - private var initialRect = initialFieldValue as Rect - - init { - rect[0] = initialRect.x.toDouble() - rect[1] = initialRect.y.toDouble() - rect[2] = initialRect.width.toDouble() - rect[3] = initialRect.height.toDouble() - - guiFieldAmount = 4 - } - - override fun init() {} - - override fun update() { - if(hasChanged()){ - initialRect = reflectionField.get(pipeline) as Rect - - rect[0] = initialRect.x.toDouble() - rect[1] = initialRect.y.toDouble() - rect[2] = initialRect.width.toDouble() - rect[3] = initialRect.height.toDouble() - - updateGuiFieldValues() - } - } - - override fun updateGuiFieldValues() { - for((i, value) in rect.withIndex()) { - fieldPanel.setFieldValue(i, value) - } - } - - override fun setGuiFieldValue(index: Int, newValue: String) { - try { - val value = newValue.toDouble() - rect[index] = value - } catch (ex: NumberFormatException) { - throw IllegalArgumentException("Parameter should be a valid numeric String") - } - - initialRect.set(rect.toDoubleArray()); - setPipelineFieldValue(initialRect) - - lastRect[0] = initialRect.x.toDouble() - lastRect[1] = initialRect.y.toDouble() - lastRect[2] = initialRect.width.toDouble() - lastRect[3] = initialRect.height.toDouble() - } - - override fun getValue(): Rect = Rect(rect.toDoubleArray()) - - override fun getGuiFieldValue(index: Int): Any = rect[index] - - override fun hasChanged(): Boolean { - hasChanged = rect[0] != lastRect[0] || rect[1] != lastRect[1] - || rect[2] != lastRect[2] || rect[3] != lastRect[3] - return hasChanged - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.tuner.field.cv + +import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.tuner.TunableField +import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField +import org.opencv.core.Rect +import org.openftc.easyopencv.OpenCvPipeline +import java.lang.reflect.Field + +@RegisterTunableField +class RectField(instance: OpenCvPipeline, reflectionField: Field, eocvSim: EOCVSim) : + TunableField(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL) { + + private var rect = arrayOf(0.0, 0.0, 0.0, 0.0) + private var lastRect = arrayOf(0.0, 0.0, 0.0, 0.0) + + @Volatile private var hasChanged = false + + private var initialRect = initialFieldValue as Rect + + init { + rect[0] = initialRect.x.toDouble() + rect[1] = initialRect.y.toDouble() + rect[2] = initialRect.width.toDouble() + rect[3] = initialRect.height.toDouble() + + guiFieldAmount = 4 + } + + override fun init() {} + + override fun update() { + if(hasChanged()){ + initialRect = reflectionField.get(pipeline) as Rect + + rect[0] = initialRect.x.toDouble() + rect[1] = initialRect.y.toDouble() + rect[2] = initialRect.width.toDouble() + rect[3] = initialRect.height.toDouble() + + updateGuiFieldValues() + } + } + + override fun updateGuiFieldValues() { + for((i, value) in rect.withIndex()) { + fieldPanel.setFieldValue(i, value) + } + } + + override fun setGuiFieldValue(index: Int, newValue: String) { + try { + val value = newValue.toDouble() + rect[index] = value + } catch (ex: NumberFormatException) { + throw IllegalArgumentException("Parameter should be a valid numeric String") + } + + initialRect.set(rect.toDoubleArray()); + setPipelineFieldValue(initialRect) + + lastRect[0] = initialRect.x.toDouble() + lastRect[1] = initialRect.y.toDouble() + lastRect[2] = initialRect.width.toDouble() + lastRect[3] = initialRect.height.toDouble() + } + + override fun getValue(): Rect = Rect(rect.toDoubleArray()) + + override fun getGuiFieldValue(index: Int): Any = rect[index] + + override fun hasChanged(): Boolean { + hasChanged = rect[0] != lastRect[0] || rect[1] != lastRect[1] + || rect[2] != lastRect[2] || rect[3] != lastRect[3] + return hasChanged + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/ScalarField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/ScalarField.java index 7906c7d5..d6d103f2 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/ScalarField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/cv/ScalarField.java @@ -1,112 +1,112 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.tuner.field.cv; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; -import com.github.serivesmejia.eocvsim.tuner.TunableField; -import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; -import org.opencv.core.Scalar; -import org.openftc.easyopencv.OpenCvPipeline; - -import java.lang.reflect.Field; -import java.util.Arrays; - -@RegisterTunableField -public class ScalarField extends TunableField { - - int scalarSize; - Scalar scalar; - - double[] lastVal = {}; - - volatile boolean hasChanged = false; - - public ScalarField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); - - scalar = (Scalar) initialFieldValue; - scalarSize = scalar.val.length; - - setGuiFieldAmount(scalarSize); - setRecommendedPanelMode(TunableFieldPanel.Mode.SLIDERS); - } - - @Override - public void init() { } - - @Override - public void update() { - try { - scalar = (Scalar) reflectionField.get(pipeline); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - - hasChanged = !Arrays.equals(scalar.val, lastVal); - - if (hasChanged) { //update values in GUI if they changed since last check - updateGuiFieldValues(); - } - - lastVal = scalar.val.clone(); - } - - @Override - public void updateGuiFieldValues() { - for (int i = 0; i < scalar.val.length; i++) { - fieldPanel.setFieldValue(i, scalar.val[i]); - } - } - - @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { - try { - scalar.val[index] = Double.parseDouble(newValue); - } catch (NumberFormatException ex) { - throw new IllegalArgumentException("Parameter should be a valid numeric String"); - } - - setPipelineFieldValue(scalar); - - lastVal = scalar.val.clone(); - } - - @Override - public Scalar getValue() { - return scalar; - } - - @Override - public Object getGuiFieldValue(int index) { - return scalar.val[index]; - } - - @Override - public boolean hasChanged() { - hasChanged = !Arrays.equals(scalar.val, lastVal); - return hasChanged; - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.tuner.field.cv; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.gui.component.tuner.TunableFieldPanel; +import com.github.serivesmejia.eocvsim.tuner.TunableField; +import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; +import org.opencv.core.Scalar; +import org.openftc.easyopencv.OpenCvPipeline; + +import java.lang.reflect.Field; +import java.util.Arrays; + +@RegisterTunableField +public class ScalarField extends TunableField { + + int scalarSize; + Scalar scalar; + + double[] lastVal = {}; + + volatile boolean hasChanged = false; + + public ScalarField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); + + scalar = (Scalar) initialFieldValue; + scalarSize = scalar.val.length; + + setGuiFieldAmount(scalarSize); + setRecommendedPanelMode(TunableFieldPanel.Mode.SLIDERS); + } + + @Override + public void init() { } + + @Override + public void update() { + try { + scalar = (Scalar) reflectionField.get(pipeline); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + + hasChanged = !Arrays.equals(scalar.val, lastVal); + + if (hasChanged) { //update values in GUI if they changed since last check + updateGuiFieldValues(); + } + + lastVal = scalar.val.clone(); + } + + @Override + public void updateGuiFieldValues() { + for (int i = 0; i < scalar.val.length; i++) { + fieldPanel.setFieldValue(i, scalar.val[i]); + } + } + + @Override + public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { + try { + scalar.val[index] = Double.parseDouble(newValue); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException("Parameter should be a valid numeric String"); + } + + setPipelineFieldValue(scalar); + + lastVal = scalar.val.clone(); + } + + @Override + public Scalar getValue() { + return scalar; + } + + @Override + public Object getGuiFieldValue(int index) { + return scalar.val[index]; + } + + @Override + public boolean hasChanged() { + hasChanged = !Arrays.equals(scalar.val, lastVal); + return hasChanged; + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/DoubleField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/DoubleField.java index 75998ee7..32dd0ef2 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/DoubleField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/DoubleField.java @@ -1,66 +1,66 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.tuner.field.numeric; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.tuner.field.NumericField; -import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; -import org.openftc.easyopencv.OpenCvPipeline; - -import java.lang.reflect.Field; - -@RegisterTunableField -public class DoubleField extends NumericField { - - private double beforeValue; - - public DoubleField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); - value = (double) initialFieldValue; - } - - @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { - - try { - value = Double.valueOf(newValue); - } catch (NumberFormatException ex) { - throw new IllegalArgumentException("Parameter should be a valid numeric String"); - } - - setPipelineFieldValue(value); - - beforeValue = value; - - } - - @Override - public boolean hasChanged() { - boolean hasChanged = value != beforeValue; - beforeValue = value; - return hasChanged; - } - - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.tuner.field.numeric; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.tuner.field.NumericField; +import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; +import org.openftc.easyopencv.OpenCvPipeline; + +import java.lang.reflect.Field; + +@RegisterTunableField +public class DoubleField extends NumericField { + + private double beforeValue; + + public DoubleField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); + value = (double) initialFieldValue; + } + + @Override + public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { + + try { + value = Double.valueOf(newValue); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException("Parameter should be a valid numeric String"); + } + + setPipelineFieldValue(value); + + beforeValue = value; + + } + + @Override + public boolean hasChanged() { + boolean hasChanged = value != beforeValue; + beforeValue = value; + return hasChanged; + } + + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/FloatField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/FloatField.java index 84e4a829..ec788b74 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/FloatField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/FloatField.java @@ -1,65 +1,65 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.tuner.field.numeric; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.tuner.field.NumericField; -import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; -import org.openftc.easyopencv.OpenCvPipeline; - -import java.lang.reflect.Field; - -@RegisterTunableField -public class FloatField extends NumericField { - - protected float beforeValue; - - public FloatField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); - value = (float) initialFieldValue; - } - - @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { - - try { - value = Float.parseFloat(newValue); - } catch (NumberFormatException ex) { - throw new IllegalArgumentException("Parameter should be a valid numeric String"); - } - - setPipelineFieldValue(value); - - beforeValue = value; - - } - - @Override - public boolean hasChanged() { - boolean hasChanged = value != beforeValue; - beforeValue = value; - return hasChanged; - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.tuner.field.numeric; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.tuner.field.NumericField; +import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; +import org.openftc.easyopencv.OpenCvPipeline; + +import java.lang.reflect.Field; + +@RegisterTunableField +public class FloatField extends NumericField { + + protected float beforeValue; + + public FloatField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS_DECIMAL); + value = (float) initialFieldValue; + } + + @Override + public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { + + try { + value = Float.parseFloat(newValue); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException("Parameter should be a valid numeric String"); + } + + setPipelineFieldValue(value); + + beforeValue = value; + + } + + @Override + public boolean hasChanged() { + boolean hasChanged = value != beforeValue; + beforeValue = value; + return hasChanged; + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/IntegerField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/IntegerField.java index 60fa12d0..99b8aa01 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/IntegerField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/IntegerField.java @@ -1,63 +1,63 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.tuner.field.numeric; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.tuner.field.NumericField; -import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; -import org.openftc.easyopencv.OpenCvPipeline; - -import java.lang.reflect.Field; - -@RegisterTunableField -public class IntegerField extends NumericField { - - protected int beforeValue; - - public IntegerField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS); - value = (int) initialFieldValue; - } - - @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { - try { - value = (int) Math.round(Double.parseDouble(newValue)); - } catch (NumberFormatException ex) { - throw new IllegalArgumentException("Parameter should be a valid numeric String"); - } - - setPipelineFieldValue(value); - - beforeValue = value; - } - - @Override - public boolean hasChanged() { - boolean hasChanged = value != beforeValue; - beforeValue = value; - return hasChanged; - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.tuner.field.numeric; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.tuner.field.NumericField; +import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; +import org.openftc.easyopencv.OpenCvPipeline; + +import java.lang.reflect.Field; + +@RegisterTunableField +public class IntegerField extends NumericField { + + protected int beforeValue; + + public IntegerField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS); + value = (int) initialFieldValue; + } + + @Override + public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { + try { + value = (int) Math.round(Double.parseDouble(newValue)); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException("Parameter should be a valid numeric String"); + } + + setPipelineFieldValue(value); + + beforeValue = value; + } + + @Override + public boolean hasChanged() { + boolean hasChanged = value != beforeValue; + beforeValue = value; + return hasChanged; + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/LongField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/LongField.java index aea7e18d..45fdff7f 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/LongField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/field/numeric/LongField.java @@ -1,63 +1,63 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.tuner.field.numeric; - -import com.github.serivesmejia.eocvsim.EOCVSim; -import com.github.serivesmejia.eocvsim.tuner.field.NumericField; -import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; -import org.openftc.easyopencv.OpenCvPipeline; - -import java.lang.reflect.Field; - -@RegisterTunableField -public class LongField extends NumericField { - - private long beforeValue; - - public LongField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { - super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS); - value = (long) initialFieldValue; - } - - @Override - public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { - try { - value = Math.round(Double.parseDouble(newValue)); - } catch (NumberFormatException ex) { - throw new IllegalArgumentException("Parameter should be a valid numeric String"); - } - - setPipelineFieldValue(value); - - beforeValue = value; - } - - @Override - public boolean hasChanged() { - boolean hasChanged = value != beforeValue; - beforeValue = value; - return hasChanged; - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.tuner.field.numeric; + +import com.github.serivesmejia.eocvsim.EOCVSim; +import com.github.serivesmejia.eocvsim.tuner.field.NumericField; +import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField; +import org.openftc.easyopencv.OpenCvPipeline; + +import java.lang.reflect.Field; + +@RegisterTunableField +public class LongField extends NumericField { + + private long beforeValue; + + public LongField(OpenCvPipeline instance, Field reflectionField, EOCVSim eocvSim) throws IllegalAccessException { + super(instance, reflectionField, eocvSim, AllowMode.ONLY_NUMBERS); + value = (long) initialFieldValue; + } + + @Override + public void setGuiFieldValue(int index, String newValue) throws IllegalAccessException { + try { + value = Math.round(Double.parseDouble(newValue)); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException("Parameter should be a valid numeric String"); + } + + setPipelineFieldValue(value); + + beforeValue = value; + } + + @Override + public boolean hasChanged() { + boolean hasChanged = value != beforeValue; + beforeValue = value; + return hasChanged; + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/scanner/AnnotatedTunableFieldScanner.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/scanner/AnnotatedTunableFieldScanner.kt index 2460a398..bd872203 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/scanner/AnnotatedTunableFieldScanner.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/scanner/AnnotatedTunableFieldScanner.kt @@ -1,95 +1,95 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.tuner.scanner - -import com.github.serivesmejia.eocvsim.tuner.TunableField -import com.github.serivesmejia.eocvsim.tuner.TunableFieldAcceptor -import com.github.serivesmejia.eocvsim.util.Log -import com.github.serivesmejia.eocvsim.util.ReflectUtil -import io.github.classgraph.ClassGraph -import java.lang.reflect.Type -import java.util.* - -@Suppress("UNCHECKED_CAST") -class AnnotatedTunableFieldScanner(private val lookInPackage: String) { - - data class ScanResult(val tunableFields: HashMap>>, - val acceptors: HashMap>, Class>) - - fun scan(): ScanResult { - val tunableFields = HashMap>>() - val acceptors = HashMap>, Class>() - - Log.info("AnnotatedTunableFieldScanner", "Scanning in $lookInPackage...") - Log.blank() - - //Scan for all classes in the specified package - val classGraph = ClassGraph().enableAnnotationInfo().acceptPackages(lookInPackage) - val result = classGraph.scan() - - //SCANNING FOR TUNABLE FIELDS - - for (classInfo in result.getClassesWithAnnotation(RegisterTunableField::class.java.name)) { - try { - val foundClass: Class<*> = try { - Class.forName(classInfo.name) - } catch (ex: ClassNotFoundException) { - Log.error("AnnotatedTunableFieldScanner", "Unable to find class ${classInfo.name}", ex) - continue //continue because we couldn't get the class... - } - - if (!ReflectUtil.hasSuperclass(foundClass, TunableField::class.java)) continue - - val foundClassTunableField = foundClass as Class> - val type = ReflectUtil.getTypeArgumentsFrom(foundClassTunableField)[0] - - Log.info( - "AnnotatedTunableFieldScanner", - "Found TunableField for " + type.typeName + " (" + foundClass.name + ")" - ) - - tunableFields[type] = foundClassTunableField - - for(innerClass in foundClass.declaredClasses) { - if (!ReflectUtil.hasSuperclass(innerClass, TunableFieldAcceptor::class.java)) continue - - acceptors[foundClass] = innerClass as Class - Log.info( - "AnnotatedTunableFieldScanner", - "Found TunableFieldAcceptor for ${foundClass.typeName} (${innerClass.name})" - ) - } - } catch (ex: Exception) { - Log.warn("AnnotatedTunableFieldScanner", "Error while processing " + classInfo.name, ex) - } - } - - Log.info("AnnotatedTunableFieldScanner", "Found " + tunableFields.size + " TunableField(s)") - Log.info("AnnotatedTunableFieldScanner", "Found " + acceptors.size + " TunableFieldAcceptors(s)") - Log.blank() - - return ScanResult(tunableFields, acceptors) - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.tuner.scanner + +import com.github.serivesmejia.eocvsim.tuner.TunableField +import com.github.serivesmejia.eocvsim.tuner.TunableFieldAcceptor +import com.github.serivesmejia.eocvsim.util.Log +import com.github.serivesmejia.eocvsim.util.ReflectUtil +import io.github.classgraph.ClassGraph +import java.lang.reflect.Type +import java.util.* + +@Suppress("UNCHECKED_CAST") +class AnnotatedTunableFieldScanner(private val lookInPackage: String) { + + data class ScanResult(val tunableFields: HashMap>>, + val acceptors: HashMap>, Class>) + + fun scan(): ScanResult { + val tunableFields = HashMap>>() + val acceptors = HashMap>, Class>() + + Log.info("AnnotatedTunableFieldScanner", "Scanning in $lookInPackage...") + Log.blank() + + //Scan for all classes in the specified package + val classGraph = ClassGraph().enableAnnotationInfo().acceptPackages(lookInPackage) + val result = classGraph.scan() + + //SCANNING FOR TUNABLE FIELDS + + for (classInfo in result.getClassesWithAnnotation(RegisterTunableField::class.java.name)) { + try { + val foundClass: Class<*> = try { + Class.forName(classInfo.name) + } catch (ex: ClassNotFoundException) { + Log.error("AnnotatedTunableFieldScanner", "Unable to find class ${classInfo.name}", ex) + continue //continue because we couldn't get the class... + } + + if (!ReflectUtil.hasSuperclass(foundClass, TunableField::class.java)) continue + + val foundClassTunableField = foundClass as Class> + val type = ReflectUtil.getTypeArgumentsFrom(foundClassTunableField)[0] + + Log.info( + "AnnotatedTunableFieldScanner", + "Found TunableField for " + type.typeName + " (" + foundClass.name + ")" + ) + + tunableFields[type] = foundClassTunableField + + for(innerClass in foundClass.declaredClasses) { + if (!ReflectUtil.hasSuperclass(innerClass, TunableFieldAcceptor::class.java)) continue + + acceptors[foundClass] = innerClass as Class + Log.info( + "AnnotatedTunableFieldScanner", + "Found TunableFieldAcceptor for ${foundClass.typeName} (${innerClass.name})" + ) + } + } catch (ex: Exception) { + Log.warn("AnnotatedTunableFieldScanner", "Error while processing " + classInfo.name, ex) + } + } + + Log.info("AnnotatedTunableFieldScanner", "Found " + tunableFields.size + " TunableField(s)") + Log.info("AnnotatedTunableFieldScanner", "Found " + acceptors.size + " TunableFieldAcceptors(s)") + Log.blank() + + return ScanResult(tunableFields, acceptors) + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/scanner/RegisterTunableField.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/scanner/RegisterTunableField.java index 3212c70f..48e2bfd0 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/scanner/RegisterTunableField.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/scanner/RegisterTunableField.java @@ -1,34 +1,34 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.tuner.scanner; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.TYPE; - -@Target({TYPE}) -@Retention(RetentionPolicy.RUNTIME) +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.tuner.scanner; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; + +@Target({TYPE}) +@Retention(RetentionPolicy.RUNTIME) public @interface RegisterTunableField { } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/scanner/RegisterTunableFieldAcceptor.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/scanner/RegisterTunableFieldAcceptor.java index c02ec58d..d692c53e 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/scanner/RegisterTunableFieldAcceptor.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/scanner/RegisterTunableFieldAcceptor.java @@ -1,38 +1,38 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.tuner.scanner; - -import com.github.serivesmejia.eocvsim.tuner.TunableField; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.TYPE; - -@Target({TYPE}) -@Retention(RetentionPolicy.RUNTIME) -public @interface RegisterTunableFieldAcceptor { - Class> tunableFieldType(); +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.tuner.scanner; + +import com.github.serivesmejia.eocvsim.tuner.TunableField; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; + +@Target({TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface RegisterTunableFieldAcceptor { + Class> tunableFieldType(); } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/CvUtil.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/CvUtil.java index bb44d490..b77d17ec 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/CvUtil.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/CvUtil.java @@ -1,181 +1,181 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.util; - -import com.github.serivesmejia.eocvsim.util.extension.CvExt; -import org.opencv.core.Mat; -import org.opencv.core.MatOfByte; -import org.opencv.core.Size; -import org.opencv.imgcodecs.Imgcodecs; -import org.opencv.imgproc.Imgproc; -import org.opencv.videoio.VideoCapture; - -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.awt.image.DataBufferByte; -import java.io.ByteArrayInputStream; -import java.io.IOException; - -public class CvUtil { - - public static void matToBufferedImage(Mat m, BufferedImage buffImg) { - // Get the BufferedImage's backing array and copy the pixels directly into it - byte[] data = ((DataBufferByte) buffImg.getRaster().getDataBuffer()).getData(); - m.get(0, 0, data); - } - - public static BufferedImage matToBufferedImage(Mat m) { - // Fastest code - // output can be assigned either to a BufferedImage or to an Image - int type = BufferedImage.TYPE_BYTE_GRAY; - if (m.channels() > 1) { - type = BufferedImage.TYPE_3BYTE_BGR; - } - - // Create an empty image in matching format - BufferedImage buffImg = new BufferedImage(m.width(), m.height(), type); - matToBufferedImage(m, buffImg); - - return buffImg; - } - - - public static boolean checkImageValid(String imagePath) { - try { - - //test if image is valid - Mat img = Imgcodecs.imread(imagePath); - - if (img != null && !img.empty()) { //image is valid - img.release(); - return true; - } else { //image is not valid - return false; - } - - } catch (Throwable ex) { - return false; - } - } - - public static boolean checkVideoValid(String videoPath) { - try { - - VideoCapture capture = new VideoCapture(); - - Mat img = new Mat(); - - capture.open(videoPath); - capture.read(img); - capture.release(); - - if (!img.empty()) { //image is valid - img.release(); - return true; - } else { //image is not valid - img.release(); - return false; - } - - } catch (Exception ex) { - return false; - } - } - - public static Size getImageSize(String imagePath) { - try { - - //test if image is valid - Mat img = Imgcodecs.imread(imagePath); - - if (img != null && !img.empty()) { //image is valid - Size size = img.size(); - img.release(); - return size; - } else { //image is not valid - return new Size(0, 0); - } - - } catch (Exception ex) { - return new Size(0, 0); - } - } - - public static Size getVideoSize(String videoPath) { - try { - - VideoCapture capture = new VideoCapture(); - - Mat img = new Mat(); - - capture.open(videoPath); - capture.read(img); - capture.release(); - - Size size = img.size(); - img.release(); - - return size; - - } catch (Exception ex) { - return new Size(); - } - } - - public static Mat readOnceFromVideo(String videoPath) { - VideoCapture capture = new VideoCapture(); - - Mat img = new Mat(); - - try { - capture.open(videoPath); - capture.read(img); - capture.release(); - - return img; - } catch (Exception ex) { - return img; - } - } - - public static Size scaleToFit(Size currentSize, Size targetSize) { - double targetAspectRatio = CvExt.aspectRatio(targetSize); - double currentAspectRatio = CvExt.aspectRatio(currentSize); - - if(currentAspectRatio == targetAspectRatio) { - return targetSize.clone(); - } else { - - double currentW = currentSize.width; - double currentH = currentSize.height; - - double widthRatio = targetSize.width / currentW; - double heightRatio = targetSize.height / currentH; - double bestRatio = Math.min(widthRatio, heightRatio); - - return new Size(currentW * bestRatio, currentH * bestRatio); - } - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.util; + +import com.github.serivesmejia.eocvsim.util.extension.CvExt; +import org.opencv.core.Mat; +import org.opencv.core.MatOfByte; +import org.opencv.core.Size; +import org.opencv.imgcodecs.Imgcodecs; +import org.opencv.imgproc.Imgproc; +import org.opencv.videoio.VideoCapture; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; +import java.io.ByteArrayInputStream; +import java.io.IOException; + +public class CvUtil { + + public static void matToBufferedImage(Mat m, BufferedImage buffImg) { + // Get the BufferedImage's backing array and copy the pixels directly into it + byte[] data = ((DataBufferByte) buffImg.getRaster().getDataBuffer()).getData(); + m.get(0, 0, data); + } + + public static BufferedImage matToBufferedImage(Mat m) { + // Fastest code + // output can be assigned either to a BufferedImage or to an Image + int type = BufferedImage.TYPE_BYTE_GRAY; + if (m.channels() > 1) { + type = BufferedImage.TYPE_3BYTE_BGR; + } + + // Create an empty image in matching format + BufferedImage buffImg = new BufferedImage(m.width(), m.height(), type); + matToBufferedImage(m, buffImg); + + return buffImg; + } + + + public static boolean checkImageValid(String imagePath) { + try { + + //test if image is valid + Mat img = Imgcodecs.imread(imagePath); + + if (img != null && !img.empty()) { //image is valid + img.release(); + return true; + } else { //image is not valid + return false; + } + + } catch (Throwable ex) { + return false; + } + } + + public static boolean checkVideoValid(String videoPath) { + try { + + VideoCapture capture = new VideoCapture(); + + Mat img = new Mat(); + + capture.open(videoPath); + capture.read(img); + capture.release(); + + if (!img.empty()) { //image is valid + img.release(); + return true; + } else { //image is not valid + img.release(); + return false; + } + + } catch (Exception ex) { + return false; + } + } + + public static Size getImageSize(String imagePath) { + try { + + //test if image is valid + Mat img = Imgcodecs.imread(imagePath); + + if (img != null && !img.empty()) { //image is valid + Size size = img.size(); + img.release(); + return size; + } else { //image is not valid + return new Size(0, 0); + } + + } catch (Exception ex) { + return new Size(0, 0); + } + } + + public static Size getVideoSize(String videoPath) { + try { + + VideoCapture capture = new VideoCapture(); + + Mat img = new Mat(); + + capture.open(videoPath); + capture.read(img); + capture.release(); + + Size size = img.size(); + img.release(); + + return size; + + } catch (Exception ex) { + return new Size(); + } + } + + public static Mat readOnceFromVideo(String videoPath) { + VideoCapture capture = new VideoCapture(); + + Mat img = new Mat(); + + try { + capture.open(videoPath); + capture.read(img); + capture.release(); + + return img; + } catch (Exception ex) { + return img; + } + } + + public static Size scaleToFit(Size currentSize, Size targetSize) { + double targetAspectRatio = CvExt.aspectRatio(targetSize); + double currentAspectRatio = CvExt.aspectRatio(currentSize); + + if(currentAspectRatio == targetAspectRatio) { + return targetSize.clone(); + } else { + + double currentW = currentSize.width; + double currentH = currentSize.height; + + double widthRatio = targetSize.width / currentW; + double heightRatio = targetSize.height / currentH; + double bestRatio = Math.min(widthRatio, heightRatio); + + return new Size(currentW * bestRatio, currentH * bestRatio); + } + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/FileFilters.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/FileFilters.kt index fa06f71a..bfed253c 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/FileFilters.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/FileFilters.kt @@ -1,38 +1,38 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.util - -import javax.swing.filechooser.FileNameExtensionFilter - -object FileFilters { - - @JvmField val imagesFilter = FileNameExtensionFilter("Images", - "jpg", "jpeg", "jpe", "jp2", "bmp", "png", "tiff", "tif") - - @JvmField var videoMediaFilter = FileNameExtensionFilter("Video Media", - "avi", "mkv", "mov", "mp4") - - @JvmField var recordedVideoFilter = FileNameExtensionFilter("AVI (*.avi)", "avi") - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.util + +import javax.swing.filechooser.FileNameExtensionFilter + +object FileFilters { + + @JvmField val imagesFilter = FileNameExtensionFilter("Images", + "jpg", "jpeg", "jpe", "jp2", "bmp", "png", "tiff", "tif") + + @JvmField var videoMediaFilter = FileNameExtensionFilter("Video Media", + "avi", "mkv", "mov", "mp4") + + @JvmField var recordedVideoFilter = FileNameExtensionFilter("AVI (*.avi)", "avi") + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/Log.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/Log.java index 226ec490..89703ea3 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/Log.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/Log.java @@ -1,277 +1,277 @@ -package com.github.serivesmejia.eocvsim.util; - -import java.io.PrintWriter; -import java.io.StringWriter; - -/** - * A low overhead, lightweight logging system. - * - * @author Nathan Sweet - */ -public class Log { - /** - * No logging at all. - */ - static public final int LEVEL_NONE = 6; - /** - * Critical errors. The application may no longer work correctly. - */ - static public final int LEVEL_ERROR = 5; - /** - * Important warnings. The application will continue to work correctly. - */ - static public final int LEVEL_WARN = 4; - /** - * Informative messages. Typically used for deployment. - */ - static public final int LEVEL_INFO = 3; - /** - * Debug messages. This level is useful during development. - */ - static public final int LEVEL_DEBUG = 2; - /** - * Trace messages. A lot of information is logged, so this level is usually only needed when debugging a problem. - */ - static public final int LEVEL_TRACE = 1; - /** - * White lines - **/ - static public final int LEVEL_BLANK = 0; - - /** - * The level of messages that will be logged. Compiling this and the booleans below as "final" will cause the compiler to - * remove all "if (Log.info) ..." type statements below the set level. - */ - static private int level = LEVEL_INFO; - - /** - * True when the ERROR level will be logged. - */ - static public boolean ERROR = level <= LEVEL_ERROR; - /** - * True when the WARN level will be logged. - */ - static public boolean WARN = level <= LEVEL_WARN; - /** - * True when the INFO level will be logged. - */ - static public boolean INFO = level <= LEVEL_INFO; - /** - * True when the DEBUG level will be logged. - */ - static public boolean DEBUG = level <= LEVEL_DEBUG; - /** - * True when the TRACE level will be logged. - */ - static public boolean TRACE = level <= LEVEL_TRACE; - static private Logger logger = new Logger(); - - public static final StringBuilder fullLogs = new StringBuilder(); - - private Log() { - } - - static public int getLevel() { - return level; - } - - /** - * Sets the level to log. If a version of this class is being used that has a final log level, this has no affect. - */ - static public void set(int level) { - // Comment out method contents when compiling fixed level JARs. - Log.level = level; - ERROR = level <= LEVEL_ERROR; - WARN = level <= LEVEL_WARN; - INFO = level <= LEVEL_INFO; - DEBUG = level <= LEVEL_DEBUG; - TRACE = level <= LEVEL_TRACE; - } - - static public void NONE() { - set(LEVEL_NONE); - } - - static public void ERROR() { - set(LEVEL_ERROR); - } - - static public void WARN() { - set(LEVEL_WARN); - } - - static public void INFO() { - set(LEVEL_INFO); - } - - static public void DEBUG() { - set(LEVEL_DEBUG); - } - - static public void TRACE() { - set(LEVEL_TRACE); - } - - /** - * Sets the logger that will write the log messages. - */ - static public void setLogger(Logger logger) { - Log.logger = logger; - } - - static public void error(String message, Throwable ex) { - if (ERROR) logger.log(LEVEL_ERROR, null, message, ex); - } - - static public void error(String category, String message, Throwable ex) { - if (ERROR) logger.log(LEVEL_ERROR, category, message, ex); - } - - static public void error(String message) { - if (ERROR) logger.log(LEVEL_ERROR, null, message, null); - } - - static public void error(String category, String message) { - if (ERROR) logger.log(LEVEL_ERROR, category, message, null); - } - - static public void warn(String message, Throwable ex) { - if (WARN) logger.log(LEVEL_WARN, null, message, ex); - } - - static public void warn(String category, String message, Throwable ex) { - if (WARN) logger.log(LEVEL_WARN, category, message, ex); - } - - static public void warn(String message) { - if (WARN) logger.log(LEVEL_WARN, null, message, null); - } - - static public void warn(String category, String message) { - if (WARN) logger.log(LEVEL_WARN, category, message, null); - } - - static public void info(String message, Throwable ex) { - if (INFO) logger.log(LEVEL_INFO, null, message, ex); - } - - static public void info(String category, String message, Throwable ex) { - if (INFO) logger.log(LEVEL_INFO, category, message, ex); - } - - static public void info(String message) { - if (INFO) logger.log(LEVEL_INFO, null, message, null); - } - - static public void info(String category, String message) { - if (INFO) logger.log(LEVEL_INFO, category, message, null); - } - - static public void debug(String message, Throwable ex) { - if (DEBUG) logger.log(LEVEL_DEBUG, null, message, ex); - } - - static public void debug(String category, String message, Throwable ex) { - if (DEBUG) logger.log(LEVEL_DEBUG, category, message, ex); - } - - static public void debug(String message) { - if (DEBUG) logger.log(LEVEL_DEBUG, null, message, null); - } - - static public void debug(String category, String message) { - if (DEBUG) logger.log(LEVEL_DEBUG, category, message, null); - } - - static public void trace(String message, Throwable ex) { - if (TRACE) logger.log(LEVEL_TRACE, null, message, ex); - } - - static public void trace(String category, String message, Throwable ex) { - if (TRACE) logger.log(LEVEL_TRACE, category, message, ex); - } - - static public void trace(String message) { - if (TRACE) logger.log(LEVEL_TRACE, null, message, null); - } - - static public void trace(String category, String message) { - if (TRACE) logger.log(LEVEL_TRACE, category, message, null); - } - - static public void blank(int lines) { - for (int i = 0; i < lines; i++) { - logger.log(LEVEL_BLANK, null, "", null); - } - } - - static public void blank() { - blank(1); - } - - /** - * Performs the actual logging. Default implementation logs to System.out. Extended and use {@link Log#logger} set to handle - * logging differently. - */ - static public class Logger { - private final long firstLogTime = System.currentTimeMillis(); - - public void log(int level, String category, String message, Throwable ex) { - StringBuilder builder = new StringBuilder(256); - - if (level != LEVEL_BLANK) { - long time = System.currentTimeMillis() - firstLogTime; - long minutes = time / (1000 * 60); - long seconds = time / (1000) % 60; - if (minutes <= 9) builder.append('0'); - builder.append(minutes); - builder.append(':'); - if (seconds <= 9) builder.append('0'); - builder.append(seconds); - } - - switch (level) { - case LEVEL_ERROR: - builder.append(" ERROR: "); - break; - case LEVEL_WARN: - builder.append(" WARN: "); - break; - case LEVEL_INFO: - builder.append(" INFO: "); - break; - case LEVEL_DEBUG: - builder.append(" DEBUG: "); - break; - case LEVEL_TRACE: - builder.append(" TRACE: "); - break; - } - - if (category != null) { - builder.append('['); - builder.append(category); - builder.append("] "); - } - - builder.append(message); - - if (ex != null) { - StringWriter writer = new StringWriter(256); - ex.printStackTrace(new PrintWriter(writer)); - builder.append('\n'); - builder.append(writer.toString().trim()); - } - - print(builder.toString()); - } - - /** - * Prints the message to System.out. Called by the default implementation of {@link #log(int, String, String, Throwable)}. - */ - protected void print(String message) { - fullLogs.append(message + "\n"); - System.out.println(message); - } - } +package com.github.serivesmejia.eocvsim.util; + +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * A low overhead, lightweight logging system. + * + * @author Nathan Sweet + */ +public class Log { + /** + * No logging at all. + */ + static public final int LEVEL_NONE = 6; + /** + * Critical errors. The application may no longer work correctly. + */ + static public final int LEVEL_ERROR = 5; + /** + * Important warnings. The application will continue to work correctly. + */ + static public final int LEVEL_WARN = 4; + /** + * Informative messages. Typically used for deployment. + */ + static public final int LEVEL_INFO = 3; + /** + * Debug messages. This level is useful during development. + */ + static public final int LEVEL_DEBUG = 2; + /** + * Trace messages. A lot of information is logged, so this level is usually only needed when debugging a problem. + */ + static public final int LEVEL_TRACE = 1; + /** + * White lines + **/ + static public final int LEVEL_BLANK = 0; + + /** + * The level of messages that will be logged. Compiling this and the booleans below as "final" will cause the compiler to + * remove all "if (Log.info) ..." type statements below the set level. + */ + static private int level = LEVEL_INFO; + + /** + * True when the ERROR level will be logged. + */ + static public boolean ERROR = level <= LEVEL_ERROR; + /** + * True when the WARN level will be logged. + */ + static public boolean WARN = level <= LEVEL_WARN; + /** + * True when the INFO level will be logged. + */ + static public boolean INFO = level <= LEVEL_INFO; + /** + * True when the DEBUG level will be logged. + */ + static public boolean DEBUG = level <= LEVEL_DEBUG; + /** + * True when the TRACE level will be logged. + */ + static public boolean TRACE = level <= LEVEL_TRACE; + static private Logger logger = new Logger(); + + public static final StringBuilder fullLogs = new StringBuilder(); + + private Log() { + } + + static public int getLevel() { + return level; + } + + /** + * Sets the level to log. If a version of this class is being used that has a final log level, this has no affect. + */ + static public void set(int level) { + // Comment out method contents when compiling fixed level JARs. + Log.level = level; + ERROR = level <= LEVEL_ERROR; + WARN = level <= LEVEL_WARN; + INFO = level <= LEVEL_INFO; + DEBUG = level <= LEVEL_DEBUG; + TRACE = level <= LEVEL_TRACE; + } + + static public void NONE() { + set(LEVEL_NONE); + } + + static public void ERROR() { + set(LEVEL_ERROR); + } + + static public void WARN() { + set(LEVEL_WARN); + } + + static public void INFO() { + set(LEVEL_INFO); + } + + static public void DEBUG() { + set(LEVEL_DEBUG); + } + + static public void TRACE() { + set(LEVEL_TRACE); + } + + /** + * Sets the logger that will write the log messages. + */ + static public void setLogger(Logger logger) { + Log.logger = logger; + } + + static public void error(String message, Throwable ex) { + if (ERROR) logger.log(LEVEL_ERROR, null, message, ex); + } + + static public void error(String category, String message, Throwable ex) { + if (ERROR) logger.log(LEVEL_ERROR, category, message, ex); + } + + static public void error(String message) { + if (ERROR) logger.log(LEVEL_ERROR, null, message, null); + } + + static public void error(String category, String message) { + if (ERROR) logger.log(LEVEL_ERROR, category, message, null); + } + + static public void warn(String message, Throwable ex) { + if (WARN) logger.log(LEVEL_WARN, null, message, ex); + } + + static public void warn(String category, String message, Throwable ex) { + if (WARN) logger.log(LEVEL_WARN, category, message, ex); + } + + static public void warn(String message) { + if (WARN) logger.log(LEVEL_WARN, null, message, null); + } + + static public void warn(String category, String message) { + if (WARN) logger.log(LEVEL_WARN, category, message, null); + } + + static public void info(String message, Throwable ex) { + if (INFO) logger.log(LEVEL_INFO, null, message, ex); + } + + static public void info(String category, String message, Throwable ex) { + if (INFO) logger.log(LEVEL_INFO, category, message, ex); + } + + static public void info(String message) { + if (INFO) logger.log(LEVEL_INFO, null, message, null); + } + + static public void info(String category, String message) { + if (INFO) logger.log(LEVEL_INFO, category, message, null); + } + + static public void debug(String message, Throwable ex) { + if (DEBUG) logger.log(LEVEL_DEBUG, null, message, ex); + } + + static public void debug(String category, String message, Throwable ex) { + if (DEBUG) logger.log(LEVEL_DEBUG, category, message, ex); + } + + static public void debug(String message) { + if (DEBUG) logger.log(LEVEL_DEBUG, null, message, null); + } + + static public void debug(String category, String message) { + if (DEBUG) logger.log(LEVEL_DEBUG, category, message, null); + } + + static public void trace(String message, Throwable ex) { + if (TRACE) logger.log(LEVEL_TRACE, null, message, ex); + } + + static public void trace(String category, String message, Throwable ex) { + if (TRACE) logger.log(LEVEL_TRACE, category, message, ex); + } + + static public void trace(String message) { + if (TRACE) logger.log(LEVEL_TRACE, null, message, null); + } + + static public void trace(String category, String message) { + if (TRACE) logger.log(LEVEL_TRACE, category, message, null); + } + + static public void blank(int lines) { + for (int i = 0; i < lines; i++) { + logger.log(LEVEL_BLANK, null, "", null); + } + } + + static public void blank() { + blank(1); + } + + /** + * Performs the actual logging. Default implementation logs to System.out. Extended and use {@link Log#logger} set to handle + * logging differently. + */ + static public class Logger { + private final long firstLogTime = System.currentTimeMillis(); + + public void log(int level, String category, String message, Throwable ex) { + StringBuilder builder = new StringBuilder(256); + + if (level != LEVEL_BLANK) { + long time = System.currentTimeMillis() - firstLogTime; + long minutes = time / (1000 * 60); + long seconds = time / (1000) % 60; + if (minutes <= 9) builder.append('0'); + builder.append(minutes); + builder.append(':'); + if (seconds <= 9) builder.append('0'); + builder.append(seconds); + } + + switch (level) { + case LEVEL_ERROR: + builder.append(" ERROR: "); + break; + case LEVEL_WARN: + builder.append(" WARN: "); + break; + case LEVEL_INFO: + builder.append(" INFO: "); + break; + case LEVEL_DEBUG: + builder.append(" DEBUG: "); + break; + case LEVEL_TRACE: + builder.append(" TRACE: "); + break; + } + + if (category != null) { + builder.append('['); + builder.append(category); + builder.append("] "); + } + + builder.append(message); + + if (ex != null) { + StringWriter writer = new StringWriter(256); + ex.printStackTrace(new PrintWriter(writer)); + builder.append('\n'); + builder.append(writer.toString().trim()); + } + + print(builder.toString()); + } + + /** + * Prints the message to System.out. Called by the default implementation of {@link #log(int, String, String, Throwable)}. + */ + protected void print(String message) { + fullLogs.append(message + "\n"); + System.out.println(message); + } + } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/ReflectUtil.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/ReflectUtil.java index e58f926b..f4820729 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/ReflectUtil.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/ReflectUtil.java @@ -1,51 +1,51 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.util; - -import java.lang.invoke.MethodType; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; - -public class ReflectUtil { - - public static boolean hasSuperclass(Class clazz, Class superClass) { - try { - clazz.asSubclass(superClass); - return true; - } catch (ClassCastException ex) { - return false; - } - } - - public static Type[] getTypeArgumentsFrom(Class clazz) { - //get type argument - Type sooper = clazz.getGenericSuperclass(); - return ((ParameterizedType)sooper).getActualTypeArguments(); - } - - public static Class wrap(Class c) { - return (Class) MethodType.methodType(c).wrap().returnType(); - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.util; + +import java.lang.invoke.MethodType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +public class ReflectUtil { + + public static boolean hasSuperclass(Class clazz, Class superClass) { + try { + clazz.asSubclass(superClass); + return true; + } catch (ClassCastException ex) { + return false; + } + } + + public static Type[] getTypeArgumentsFrom(Class clazz) { + //get type argument + Type sooper = clazz.getGenericSuperclass(); + return ((ParameterizedType)sooper).getActualTypeArguments(); + } + + public static Class wrap(Class c) { + return (Class) MethodType.methodType(c).wrap().returnType(); + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/StrUtil.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/StrUtil.java index 60ea0d77..257f645e 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/StrUtil.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/StrUtil.java @@ -1,85 +1,85 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.util; - -import java.awt.*; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.net.URI; -import java.util.ArrayList; -import java.util.UUID; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public final class StrUtil { - - public static final Pattern URL_PATTERN = Pattern.compile( - "((https?|ftp|gopher|telnet|file):((//)|(\\\\))+[\\w\\d:#@%/;$()~_?\\+-=\\\\\\.&]*)", - Pattern.CASE_INSENSITIVE); - - public static String[] findUrlsInString(String str) { - - Matcher urlMatcher = URL_PATTERN.matcher(str); - - ArrayList matches = new ArrayList<>(); - - while(urlMatcher.find()) { - String url = str.substring(urlMatcher.start(0), - urlMatcher.end(0)); - matches.add(url); - } - - return matches.toArray(new String[0]); - - } - - public static String getFileBaseName(String fileName) { - int index = fileName.lastIndexOf('.'); - if(index == -1) - return fileName; - else - return fileName.substring(0, index); - } - - public static String random() { - return UUID.randomUUID().toString().replace("-", ""); - } - - public static String fromException(Throwable ex) { - StringWriter writer = new StringWriter(256); - ex.printStackTrace(new PrintWriter(writer)); - return writer.toString().trim(); - } - - public static String cutStringBy(String str, String by, int amount) { - int truncateIndex = str.length(); - - for(int i = 0 ; i < amount ; i++) { - truncateIndex = str.lastIndexOf(by, truncateIndex - 1); - } - - return str.substring(0, truncateIndex); - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.util; + +import java.awt.*; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.URI; +import java.util.ArrayList; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class StrUtil { + + public static final Pattern URL_PATTERN = Pattern.compile( + "((https?|ftp|gopher|telnet|file):((//)|(\\\\))+[\\w\\d:#@%/;$()~_?\\+-=\\\\\\.&]*)", + Pattern.CASE_INSENSITIVE); + + public static String[] findUrlsInString(String str) { + + Matcher urlMatcher = URL_PATTERN.matcher(str); + + ArrayList matches = new ArrayList<>(); + + while(urlMatcher.find()) { + String url = str.substring(urlMatcher.start(0), + urlMatcher.end(0)); + matches.add(url); + } + + return matches.toArray(new String[0]); + + } + + public static String getFileBaseName(String fileName) { + int index = fileName.lastIndexOf('.'); + if(index == -1) + return fileName; + else + return fileName.substring(0, index); + } + + public static String random() { + return UUID.randomUUID().toString().replace("-", ""); + } + + public static String fromException(Throwable ex) { + StringWriter writer = new StringWriter(256); + ex.printStackTrace(new PrintWriter(writer)); + return writer.toString().trim(); + } + + public static String cutStringBy(String str, String by, int amount) { + int truncateIndex = str.length(); + + for(int i = 0 ; i < amount ; i++) { + truncateIndex = str.lastIndexOf(by, truncateIndex - 1); + } + + return str.substring(0, truncateIndex); + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/SysUtil.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/SysUtil.java index 0aeeabda..884e6545 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/SysUtil.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/SysUtil.java @@ -1,385 +1,385 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.util; - -import com.github.serivesmejia.eocvsim.util.io.EOCVSimFolder; -import org.opencv.core.Core; - -import java.io.*; -import java.net.URI; -import java.nio.charset.Charset; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -public class SysUtil { - - public static OperatingSystem OS = SysUtil.getOS(); - public static int MB = 1024 * 1024; - public static String GH_NATIVE_LIBS_URL = "https://github.com/serivesmejia/OpenCVNativeLibs/raw/master/"; - - public static OperatingSystem getOS() { - String osName = System.getProperty("os.name").toLowerCase(); - - if (osName.contains("win")) { - return OperatingSystem.WINDOWS; - } else if (osName.contains("nux")) { - return OperatingSystem.LINUX; - } else if (osName.contains("mac") || osName.contains("darwin")) { - return OperatingSystem.MACOS; - } - - return OperatingSystem.UNKNOWN; - } - - public static boolean loadCvNativeLib() { - String os = null; - String fileExt = null; - - switch (OS) { //getting os prefix - case WINDOWS: - os = "win"; - fileExt = "dll"; - break; - case LINUX: - os = "linux"; - fileExt = "so"; - break; - case MACOS: - os = "mac"; - fileExt = "dylib"; - break; - } - - boolean is64bit = System.getProperty("sun.arch.data.model").contains("64"); //Checking if JVM is 64 bits or not - - return loadLib(os, fileExt, is64bit, Core.NATIVE_LIBRARY_NAME, 0); - } - - public static boolean loadLib(String os, String fileExt, boolean is64bit, String name, int attempts) { - String arch = is64bit ? "64" : "32"; //getting os arch - - String libName = os + arch + "_" + name; //resultant lib name from those two - String libNameExt = libName + "." + fileExt; //resultant lib name from those two - - File nativeLibFile = new File(getAppData() + File.separator + libNameExt); - - if (!nativeLibFile.exists()) { - Log.info("SysUtil", "Downloading native lib from " + GH_NATIVE_LIBS_URL + libNameExt); - try { - download(GH_NATIVE_LIBS_URL + libNameExt, nativeLibFile.getAbsolutePath()); - } catch (Throwable ex) { - ex.printStackTrace(); - } - Log.blank(); - } - - Log.info("SysUtil", "Loading native lib \"" + libNameExt + "\""); - - try { - - System.load(nativeLibFile.getAbsolutePath()); //Loading OpenCV native library - Log.info("SysUtil", "Successfully loaded native lib \"" + libName + "\""); - - } catch (UnsatisfiedLinkError ex) { - ex.printStackTrace(); - - if (attempts < 4) { - ex.printStackTrace(); - Log.error("SysUtil", "Failure loading lib \"" + libName + "\", retrying with different architecture... (" + attempts + " attempts)"); - loadLib(os, fileExt, !is64bit, Core.NATIVE_LIBRARY_NAME, attempts + 1); - } else { - ex.printStackTrace(); - Log.error("SysUtil", "Failure loading lib \"" + libName + "\" 4 times, giving up."); - return false; - } - } - - return true; - } - - public static void copyStream(File inFile, OutputStream out) throws IOException { - try (InputStream in = new FileInputStream(inFile)) { - copyStream(in, out); - } - } - - public static void copyStream(InputStream in, OutputStream out) throws IOException { - int cbBuffer = Math.min(4096, in.available()); - byte[] buffer = new byte[cbBuffer]; - - while(true) { - int cbRead = in.read(buffer); - if(cbRead <= 0) break; - - out.write(buffer, 0, cbRead); - } - } - - public static CopyFileIsData copyFileIs(InputStream is, File toPath, boolean replaceIfExisting) throws IOException { - - boolean alreadyExists = true; - - if (toPath.exists()) { - if (replaceIfExisting) { - Files.copy(is, toPath.toPath(), StandardCopyOption.REPLACE_EXISTING); - } else { - alreadyExists = false; - } - } else { - Files.copy(is, toPath.toPath(), StandardCopyOption.REPLACE_EXISTING); - } - - is.close(); - - CopyFileIsData data = new CopyFileIsData(); - data.alreadyExists = alreadyExists; - data.file = toPath; - - return data; - - } - - public static CopyFileIsData copyFileIsTemp(InputStream is, String fileName, boolean replaceIfExisting) throws IOException { - String tmpDir = System.getProperty("java.io.tmpdir"); - File tempFile = new File(tmpDir + File.separator + fileName); - - return copyFileIs(is, tempFile, replaceIfExisting); - } - - public static long getMemoryUsageMB() { - return (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / MB; - } - - public static String loadIsStr(InputStream is, Charset charset) throws UnsupportedEncodingException { - return new BufferedReader(new InputStreamReader(is, String.valueOf(charset))) - .lines().collect(Collectors.joining("\n")); - } - - public static String loadFileStr(File f) { - String content = ""; - - try { - content = new String(Files.readAllBytes(f.toPath())); - } catch (IOException e) { - e.printStackTrace(); - } - - return content; - } - - public static void replaceStrInFile(File f, String target, String replacement) { - String fileContents = loadFileStr(f); - saveFileStr(f, fileContents.replace(target, replacement)); - } - - public static boolean saveFileStr(File f, String contents) { - try { - FileWriter fw = new FileWriter(f); - fw.append(contents); - fw.close(); - return true; - } catch (IOException e) { - e.printStackTrace(); - return false; - } - } - - public static void download(String url, String fileName) throws Exception { - try (InputStream in = URI.create(url).toURL().openStream()) { - Files.copy(in, Paths.get(fileName)); - } - } - - public static File getAppData() { - return new File(System.getProperty("user.home") + File.separator); - } - - public static File getEOCVSimFolder() { - return EOCVSimFolder.INSTANCE; - } - - public static Optional getExtensionByStringHandling(String filename) { - return Optional.ofNullable(filename) - .filter(f -> f.contains(".")) - .map(f -> f.substring(filename.lastIndexOf(".") + 1)); - } - - public static List filesUnder(File parent, Predicate predicate) { - ArrayList result = new ArrayList<>(); - - if(parent.isDirectory()) { - for(File child : parent.listFiles()) { - result.addAll(filesUnder(child, predicate)); - } - } else if(parent.exists() && (predicate != null && predicate.test(parent))) { - result.add(parent.getAbsoluteFile()); - } - - return result; - } - - public static List filesUnder(File parent, String extension) { - return filesUnder(parent, (f) -> f.getName().endsWith(extension)); - } - - public static List filesUnder(File parent) { - return filesUnder(parent, (f) -> true); - } - - public static List filesIn(File parent, Predicate predicate) { - ArrayList result = new ArrayList<>(); - - if(!parent.exists()) return result; - - if(parent.isDirectory()) { - for(File f : parent.listFiles()) { - if(predicate != null && predicate.test(f)) - result.add(f); - } - } else { - if(predicate != null && predicate.test(parent)) - result.add(parent); - } - - return result; - } - - public static List filesIn(File parent, String extension) { - return filesIn(parent, (f) -> f.getName().endsWith(extension)); - } - - public static void deleteFilesUnder(File parent, Predicate predicate) { - for(File file : parent.listFiles()) { - if(file.isDirectory()) - deleteFilesUnder(file, predicate); - - if(predicate != null) { - if(predicate.test(file)) file.delete(); - } else { - file.delete(); - } - } - } - - public static void deleteFilesUnder(File parent) { - deleteFilesUnder(parent, null); - } - - public static boolean migrateFile(File oldFile, File newFile) { - if(newFile.exists() || !oldFile.exists()) return false; - - Log.info("SysUtil", "Migrating old file " + oldFile.getAbsolutePath() + " to " + newFile.getAbsolutePath()); - - try { - Files.move(oldFile.toPath(), newFile.toPath()); - } catch (IOException e) { - Log.warn("SysUtil", "Failed to migrate old file " + oldFile.getAbsolutePath()); - return false; - } - - return true; - } - - public static File getRelativePath(File root, File child) { - File result = new File(""); - - while(!root.equals(child)) { - File parent = child.getParentFile(); - result = new File(new File(child.getName()), result.getPath()); - - if(parent == null) break; - - child = parent; - } - - return result; - } - - public static List getClasspathFiles() { - String[] classpaths = System.getProperty("java.class.path").split(File.pathSeparator); - ArrayList files = new ArrayList<>(); - - for(String path : classpaths) { - files.add(new File(path)); - } - - return files; - } - - public static CommandResult runShellCommand(String command) { - CommandResult result = new CommandResult(); - - ProcessBuilder processBuilder = new ProcessBuilder(); - if (OS == OperatingSystem.WINDOWS) { - processBuilder.command("cmd.exe", "/c", command); - } else { - processBuilder.command("sh", "-c", command); - } - - try { - Process process = processBuilder.start(); - BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); - - String line = ""; - StringBuilder message = new StringBuilder(); - - while((line = reader.readLine()) != null) { - message.append(line); - } - - result.exitCode = process.waitFor(); - - result.output = message.toString(); - } catch (IOException | InterruptedException e) { - result.output = StrUtil.fromException(e); - result.exitCode = -1; - } - - return result; - } - - public enum OperatingSystem { - WINDOWS, - LINUX, - MACOS, - UNKNOWN - } - - public static class CopyFileIsData { - public File file = null; - public boolean alreadyExists = false; - } - - public static class CommandResult { - public String output = ""; - public int exitCode = 0; - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.util; + +import com.github.serivesmejia.eocvsim.util.io.EOCVSimFolder; +import org.opencv.core.Core; + +import java.io.*; +import java.net.URI; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class SysUtil { + + public static OperatingSystem OS = SysUtil.getOS(); + public static int MB = 1024 * 1024; + public static String GH_NATIVE_LIBS_URL = "https://github.com/serivesmejia/OpenCVNativeLibs/raw/master/"; + + public static OperatingSystem getOS() { + String osName = System.getProperty("os.name").toLowerCase(); + + if (osName.contains("win")) { + return OperatingSystem.WINDOWS; + } else if (osName.contains("nux")) { + return OperatingSystem.LINUX; + } else if (osName.contains("mac") || osName.contains("darwin")) { + return OperatingSystem.MACOS; + } + + return OperatingSystem.UNKNOWN; + } + + public static boolean loadCvNativeLib() { + String os = null; + String fileExt = null; + + switch (OS) { //getting os prefix + case WINDOWS: + os = "win"; + fileExt = "dll"; + break; + case LINUX: + os = "linux"; + fileExt = "so"; + break; + case MACOS: + os = "mac"; + fileExt = "dylib"; + break; + } + + boolean is64bit = System.getProperty("sun.arch.data.model").contains("64"); //Checking if JVM is 64 bits or not + + return loadLib(os, fileExt, is64bit, Core.NATIVE_LIBRARY_NAME, 0); + } + + public static boolean loadLib(String os, String fileExt, boolean is64bit, String name, int attempts) { + String arch = is64bit ? "64" : "32"; //getting os arch + + String libName = os + arch + "_" + name; //resultant lib name from those two + String libNameExt = libName + "." + fileExt; //resultant lib name from those two + + File nativeLibFile = new File(getAppData() + File.separator + libNameExt); + + if (!nativeLibFile.exists()) { + Log.info("SysUtil", "Downloading native lib from " + GH_NATIVE_LIBS_URL + libNameExt); + try { + download(GH_NATIVE_LIBS_URL + libNameExt, nativeLibFile.getAbsolutePath()); + } catch (Throwable ex) { + ex.printStackTrace(); + } + Log.blank(); + } + + Log.info("SysUtil", "Loading native lib \"" + libNameExt + "\""); + + try { + + System.load(nativeLibFile.getAbsolutePath()); //Loading OpenCV native library + Log.info("SysUtil", "Successfully loaded native lib \"" + libName + "\""); + + } catch (UnsatisfiedLinkError ex) { + ex.printStackTrace(); + + if (attempts < 4) { + ex.printStackTrace(); + Log.error("SysUtil", "Failure loading lib \"" + libName + "\", retrying with different architecture... (" + attempts + " attempts)"); + loadLib(os, fileExt, !is64bit, Core.NATIVE_LIBRARY_NAME, attempts + 1); + } else { + ex.printStackTrace(); + Log.error("SysUtil", "Failure loading lib \"" + libName + "\" 4 times, giving up."); + return false; + } + } + + return true; + } + + public static void copyStream(File inFile, OutputStream out) throws IOException { + try (InputStream in = new FileInputStream(inFile)) { + copyStream(in, out); + } + } + + public static void copyStream(InputStream in, OutputStream out) throws IOException { + int cbBuffer = Math.min(4096, in.available()); + byte[] buffer = new byte[cbBuffer]; + + while(true) { + int cbRead = in.read(buffer); + if(cbRead <= 0) break; + + out.write(buffer, 0, cbRead); + } + } + + public static CopyFileIsData copyFileIs(InputStream is, File toPath, boolean replaceIfExisting) throws IOException { + + boolean alreadyExists = true; + + if (toPath.exists()) { + if (replaceIfExisting) { + Files.copy(is, toPath.toPath(), StandardCopyOption.REPLACE_EXISTING); + } else { + alreadyExists = false; + } + } else { + Files.copy(is, toPath.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + + is.close(); + + CopyFileIsData data = new CopyFileIsData(); + data.alreadyExists = alreadyExists; + data.file = toPath; + + return data; + + } + + public static CopyFileIsData copyFileIsTemp(InputStream is, String fileName, boolean replaceIfExisting) throws IOException { + String tmpDir = System.getProperty("java.io.tmpdir"); + File tempFile = new File(tmpDir + File.separator + fileName); + + return copyFileIs(is, tempFile, replaceIfExisting); + } + + public static long getMemoryUsageMB() { + return (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / MB; + } + + public static String loadIsStr(InputStream is, Charset charset) throws UnsupportedEncodingException { + return new BufferedReader(new InputStreamReader(is, String.valueOf(charset))) + .lines().collect(Collectors.joining("\n")); + } + + public static String loadFileStr(File f) { + String content = ""; + + try { + content = new String(Files.readAllBytes(f.toPath())); + } catch (IOException e) { + e.printStackTrace(); + } + + return content; + } + + public static void replaceStrInFile(File f, String target, String replacement) { + String fileContents = loadFileStr(f); + saveFileStr(f, fileContents.replace(target, replacement)); + } + + public static boolean saveFileStr(File f, String contents) { + try { + FileWriter fw = new FileWriter(f); + fw.append(contents); + fw.close(); + return true; + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + public static void download(String url, String fileName) throws Exception { + try (InputStream in = URI.create(url).toURL().openStream()) { + Files.copy(in, Paths.get(fileName)); + } + } + + public static File getAppData() { + return new File(System.getProperty("user.home") + File.separator); + } + + public static File getEOCVSimFolder() { + return EOCVSimFolder.INSTANCE; + } + + public static Optional getExtensionByStringHandling(String filename) { + return Optional.ofNullable(filename) + .filter(f -> f.contains(".")) + .map(f -> f.substring(filename.lastIndexOf(".") + 1)); + } + + public static List filesUnder(File parent, Predicate predicate) { + ArrayList result = new ArrayList<>(); + + if(parent.isDirectory()) { + for(File child : parent.listFiles()) { + result.addAll(filesUnder(child, predicate)); + } + } else if(parent.exists() && (predicate != null && predicate.test(parent))) { + result.add(parent.getAbsoluteFile()); + } + + return result; + } + + public static List filesUnder(File parent, String extension) { + return filesUnder(parent, (f) -> f.getName().endsWith(extension)); + } + + public static List filesUnder(File parent) { + return filesUnder(parent, (f) -> true); + } + + public static List filesIn(File parent, Predicate predicate) { + ArrayList result = new ArrayList<>(); + + if(!parent.exists()) return result; + + if(parent.isDirectory()) { + for(File f : parent.listFiles()) { + if(predicate != null && predicate.test(f)) + result.add(f); + } + } else { + if(predicate != null && predicate.test(parent)) + result.add(parent); + } + + return result; + } + + public static List filesIn(File parent, String extension) { + return filesIn(parent, (f) -> f.getName().endsWith(extension)); + } + + public static void deleteFilesUnder(File parent, Predicate predicate) { + for(File file : parent.listFiles()) { + if(file.isDirectory()) + deleteFilesUnder(file, predicate); + + if(predicate != null) { + if(predicate.test(file)) file.delete(); + } else { + file.delete(); + } + } + } + + public static void deleteFilesUnder(File parent) { + deleteFilesUnder(parent, null); + } + + public static boolean migrateFile(File oldFile, File newFile) { + if(newFile.exists() || !oldFile.exists()) return false; + + Log.info("SysUtil", "Migrating old file " + oldFile.getAbsolutePath() + " to " + newFile.getAbsolutePath()); + + try { + Files.move(oldFile.toPath(), newFile.toPath()); + } catch (IOException e) { + Log.warn("SysUtil", "Failed to migrate old file " + oldFile.getAbsolutePath()); + return false; + } + + return true; + } + + public static File getRelativePath(File root, File child) { + File result = new File(""); + + while(!root.equals(child)) { + File parent = child.getParentFile(); + result = new File(new File(child.getName()), result.getPath()); + + if(parent == null) break; + + child = parent; + } + + return result; + } + + public static List getClasspathFiles() { + String[] classpaths = System.getProperty("java.class.path").split(File.pathSeparator); + ArrayList files = new ArrayList<>(); + + for(String path : classpaths) { + files.add(new File(path)); + } + + return files; + } + + public static CommandResult runShellCommand(String command) { + CommandResult result = new CommandResult(); + + ProcessBuilder processBuilder = new ProcessBuilder(); + if (OS == OperatingSystem.WINDOWS) { + processBuilder.command("cmd.exe", "/c", command); + } else { + processBuilder.command("sh", "-c", command); + } + + try { + Process process = processBuilder.start(); + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + + String line = ""; + StringBuilder message = new StringBuilder(); + + while((line = reader.readLine()) != null) { + message.append(line); + } + + result.exitCode = process.waitFor(); + + result.output = message.toString(); + } catch (IOException | InterruptedException e) { + result.output = StrUtil.fromException(e); + result.exitCode = -1; + } + + return result; + } + + public enum OperatingSystem { + WINDOWS, + LINUX, + MACOS, + UNKNOWN + } + + public static class CopyFileIsData { + public File file = null; + public boolean alreadyExists = false; + } + + public static class CommandResult { + public String output = ""; + public int exitCode = 0; + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/compiler/DelegatingStandardFileManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/compiler/DelegatingStandardFileManager.kt index 3e4dccd4..59c120b6 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/compiler/DelegatingStandardFileManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/compiler/DelegatingStandardFileManager.kt @@ -1,56 +1,56 @@ -/* - * Copyright (c) 2021 Sebastian Erives & (c) 2017 Robert Atkinson - * - * Based from the FTC SDK's org.firstinspires.ftc.onbotjava.OnBotJavaDelegatingStandardFileManager - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.util.compiler - -import java.io.File -import javax.tools.ForwardingJavaFileManager -import javax.tools.JavaFileManager -import javax.tools.JavaFileObject -import javax.tools.StandardJavaFileManager - -open class DelegatingStandardFileManager( - val delegate: StandardJavaFileManager -) : ForwardingJavaFileManager(delegate), StandardJavaFileManager { - - override fun getJavaFileObjectsFromFiles(files: MutableIterable): MutableIterable = - delegate.getJavaFileObjectsFromFiles(files) - - override fun getJavaFileObjects(vararg files: File): MutableIterable = - delegate.getJavaFileObjects(*files) - - override fun getJavaFileObjects(vararg names: String): MutableIterable = - delegate.getJavaFileObjects(*names) - - override fun getJavaFileObjectsFromStrings(names: MutableIterable): MutableIterable = - delegate.getJavaFileObjectsFromStrings(names) - - override fun setLocation(location: JavaFileManager.Location, files: MutableIterable) = - delegate.setLocation(location, files) - - override fun getLocation(location: JavaFileManager.Location): MutableIterable = - delegate.getLocation(location) - +/* + * Copyright (c) 2021 Sebastian Erives & (c) 2017 Robert Atkinson + * + * Based from the FTC SDK's org.firstinspires.ftc.onbotjava.OnBotJavaDelegatingStandardFileManager + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.util.compiler + +import java.io.File +import javax.tools.ForwardingJavaFileManager +import javax.tools.JavaFileManager +import javax.tools.JavaFileObject +import javax.tools.StandardJavaFileManager + +open class DelegatingStandardFileManager( + val delegate: StandardJavaFileManager +) : ForwardingJavaFileManager(delegate), StandardJavaFileManager { + + override fun getJavaFileObjectsFromFiles(files: MutableIterable): MutableIterable = + delegate.getJavaFileObjectsFromFiles(files) + + override fun getJavaFileObjects(vararg files: File): MutableIterable = + delegate.getJavaFileObjects(*files) + + override fun getJavaFileObjects(vararg names: String): MutableIterable = + delegate.getJavaFileObjects(*names) + + override fun getJavaFileObjectsFromStrings(names: MutableIterable): MutableIterable = + delegate.getJavaFileObjectsFromStrings(names) + + override fun setLocation(location: JavaFileManager.Location, files: MutableIterable) = + delegate.setLocation(location, files) + + override fun getLocation(location: JavaFileManager.Location): MutableIterable = + delegate.getLocation(location) + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/compiler/JarPacker.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/compiler/JarPacker.kt index c99899dc..39224508 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/compiler/JarPacker.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/compiler/JarPacker.kt @@ -1,78 +1,78 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.util.compiler - -import com.github.serivesmejia.eocvsim.util.SysUtil -import java.io.File -import java.io.FileOutputStream -import java.util.jar.JarOutputStream -import java.util.jar.Manifest -import java.util.zip.ZipEntry - -object JarPacker { - - private fun pack(outputJar: File, inputClasses: File, - resourceFilesRoot: File? = null, - resourceFiles: List? = null, - manifest: Manifest = Manifest()) { - - FileOutputStream(outputJar).use { outStream -> - JarOutputStream(outStream, manifest).use { jarOutStream -> - for (classFile in SysUtil.filesUnder(inputClasses, ".class")) { - putFileInJar(jarOutStream, inputClasses, classFile) - } - - if(resourceFiles != null && resourceFilesRoot != null) { - for(resFile in resourceFiles) { - putFileInJar(jarOutStream, resourceFilesRoot, resFile) - } - } - } - } - - } - - fun packClassesUnder(outputJar: File, - inputClasses: File, - manifest: Manifest = Manifest()) = pack(outputJar, inputClasses, manifest = manifest) - - fun packResAndClassesUnder(outputJar: File, - inputClasses: File, - resourceFilesRoot: File, - resourceFiles: List, - manifest: Manifest = Manifest()) = - pack(outputJar, inputClasses, resourceFilesRoot, resourceFiles, manifest) - - private fun putFileInJar(jar: JarOutputStream, rootFile: File, file: File) { - if(!file.exists()) return - - val ze = ZipEntry(SysUtil.getRelativePath(rootFile, file).path) - ze.time = file.lastModified() - - jar.putNextEntry(ze) - SysUtil.copyStream(file, jar) - jar.closeEntry() - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.util.compiler + +import com.github.serivesmejia.eocvsim.util.SysUtil +import java.io.File +import java.io.FileOutputStream +import java.util.jar.JarOutputStream +import java.util.jar.Manifest +import java.util.zip.ZipEntry + +object JarPacker { + + private fun pack(outputJar: File, inputClasses: File, + resourceFilesRoot: File? = null, + resourceFiles: List? = null, + manifest: Manifest = Manifest()) { + + FileOutputStream(outputJar).use { outStream -> + JarOutputStream(outStream, manifest).use { jarOutStream -> + for (classFile in SysUtil.filesUnder(inputClasses, ".class")) { + putFileInJar(jarOutStream, inputClasses, classFile) + } + + if(resourceFiles != null && resourceFilesRoot != null) { + for(resFile in resourceFiles) { + putFileInJar(jarOutStream, resourceFilesRoot, resFile) + } + } + } + } + + } + + fun packClassesUnder(outputJar: File, + inputClasses: File, + manifest: Manifest = Manifest()) = pack(outputJar, inputClasses, manifest = manifest) + + fun packResAndClassesUnder(outputJar: File, + inputClasses: File, + resourceFilesRoot: File, + resourceFiles: List, + manifest: Manifest = Manifest()) = + pack(outputJar, inputClasses, resourceFilesRoot, resourceFiles, manifest) + + private fun putFileInJar(jar: JarOutputStream, rootFile: File, file: File) { + if(!file.exists()) return + + val ze = ZipEntry(SysUtil.getRelativePath(rootFile, file).path) + ze.time = file.lastModified() + + jar.putNextEntry(ze) + SysUtil.copyStream(file, jar) + jar.closeEntry() + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt index 97bf5d58..ed9b2e0d 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventHandler.kt @@ -1,142 +1,142 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.util.event - -import com.github.serivesmejia.eocvsim.util.Log - -class EventHandler(val name: String) : Runnable { - - private val lock = Any() - private val onceLock = Any() - - val listeners: Array - get() { - synchronized(lock) { - return internalListeners.toTypedArray() - } - } - - val onceListeners: Array - get() { - synchronized(onceLock) { - return internalOnceListeners.toTypedArray() - } - } - - var callRightAway = false - - private val internalListeners = ArrayList() - private val internalOnceListeners = ArrayList() - - override fun run() { - for(listener in listeners) { - try { - runListener(listener, false) - } catch (ex: Exception) { - if(ex is InterruptedException) { - Log.warn("${name}-EventHandler", "Rethrowing InterruptedException...") - throw ex - } else { - Log.warn("${name}-EventHandler", "Error while running listener ${listener.javaClass.name}", ex) - } - } - } - - val toRemoveOnceListeners = mutableListOf() - - //executing "doOnce" listeners - for(listener in onceListeners) { - try { - runListener(listener, true) - } catch (ex: Exception) { - if(ex is InterruptedException) { - Log.warn("${name}-EventHandler", "Rethrowing InterruptedException...") - throw ex - } else { - Log.warn("${name}-EventHandler", "Error while running \"once\" ${listener.javaClass.name}", ex) - } - } - - toRemoveOnceListeners.add(listener) - } - - synchronized(onceLock) { - for(listener in toRemoveOnceListeners) { - internalOnceListeners.remove(listener) - } - } - } - - fun doOnce(listener: EventListener) { - if(callRightAway) - runListener(listener, true) - else synchronized(onceLock) { - internalOnceListeners.add(listener) - } - } - - fun doOnce(runnable: Runnable) = doOnce { runnable.run() } - - - fun doPersistent(listener: EventListener) { - synchronized(lock) { - internalListeners.add(listener) - } - - if(callRightAway) runListener(listener, false) - } - - fun doPersistent(runnable: Runnable) = doPersistent { runnable.run() } - - fun removePersistentListener(listener: EventListener) { - if(internalListeners.contains(listener)) { - synchronized(lock) { internalListeners.remove(listener) } - } - } - - fun removeOnceListener(listener: EventListener) { - if(internalOnceListeners.contains(listener)) { - synchronized(onceLock) { internalOnceListeners.remove(listener) } - } - } - - fun removeAllListeners() { - removeAllPersistentListeners() - removeAllOnceListeners() - } - - fun removeAllPersistentListeners() = synchronized(lock) { - internalListeners.clear() - } - - fun removeAllOnceListeners() = synchronized(onceLock) { - internalOnceListeners.clear() - } - - operator fun invoke(listener: EventListener) = doPersistent(listener) - - private fun runListener(listener: EventListener, isOnce: Boolean) = - listener.run(EventListenerRemover(this, listener, isOnce)) - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.util.event + +import com.github.serivesmejia.eocvsim.util.Log + +class EventHandler(val name: String) : Runnable { + + private val lock = Any() + private val onceLock = Any() + + val listeners: Array + get() { + synchronized(lock) { + return internalListeners.toTypedArray() + } + } + + val onceListeners: Array + get() { + synchronized(onceLock) { + return internalOnceListeners.toTypedArray() + } + } + + var callRightAway = false + + private val internalListeners = ArrayList() + private val internalOnceListeners = ArrayList() + + override fun run() { + for(listener in listeners) { + try { + runListener(listener, false) + } catch (ex: Exception) { + if(ex is InterruptedException) { + Log.warn("${name}-EventHandler", "Rethrowing InterruptedException...") + throw ex + } else { + Log.warn("${name}-EventHandler", "Error while running listener ${listener.javaClass.name}", ex) + } + } + } + + val toRemoveOnceListeners = mutableListOf() + + //executing "doOnce" listeners + for(listener in onceListeners) { + try { + runListener(listener, true) + } catch (ex: Exception) { + if(ex is InterruptedException) { + Log.warn("${name}-EventHandler", "Rethrowing InterruptedException...") + throw ex + } else { + Log.warn("${name}-EventHandler", "Error while running \"once\" ${listener.javaClass.name}", ex) + } + } + + toRemoveOnceListeners.add(listener) + } + + synchronized(onceLock) { + for(listener in toRemoveOnceListeners) { + internalOnceListeners.remove(listener) + } + } + } + + fun doOnce(listener: EventListener) { + if(callRightAway) + runListener(listener, true) + else synchronized(onceLock) { + internalOnceListeners.add(listener) + } + } + + fun doOnce(runnable: Runnable) = doOnce { runnable.run() } + + + fun doPersistent(listener: EventListener) { + synchronized(lock) { + internalListeners.add(listener) + } + + if(callRightAway) runListener(listener, false) + } + + fun doPersistent(runnable: Runnable) = doPersistent { runnable.run() } + + fun removePersistentListener(listener: EventListener) { + if(internalListeners.contains(listener)) { + synchronized(lock) { internalListeners.remove(listener) } + } + } + + fun removeOnceListener(listener: EventListener) { + if(internalOnceListeners.contains(listener)) { + synchronized(onceLock) { internalOnceListeners.remove(listener) } + } + } + + fun removeAllListeners() { + removeAllPersistentListeners() + removeAllOnceListeners() + } + + fun removeAllPersistentListeners() = synchronized(lock) { + internalListeners.clear() + } + + fun removeAllOnceListeners() = synchronized(onceLock) { + internalOnceListeners.clear() + } + + operator fun invoke(listener: EventListener) = doPersistent(listener) + + private fun runListener(listener: EventListener, isOnce: Boolean) = + listener.run(EventListenerRemover(this, listener, isOnce)) + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventListener.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventListener.kt index 3285909f..532802f9 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventListener.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/event/EventListener.kt @@ -1,42 +1,42 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.util.event - -fun interface EventListener { - fun run(remover: EventListenerRemover) -} - -class EventListenerRemover( - val handler: EventHandler, - val listener: EventListener, - val isOnceListener: Boolean -) { - fun removeThis() { - if(isOnceListener) - handler.removeOnceListener(listener) - else - handler.removePersistentListener(listener) - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.util.event + +fun interface EventListener { + fun run(remover: EventListenerRemover) +} + +class EventListenerRemover( + val handler: EventHandler, + val listener: EventListener, + val isOnceListener: Boolean +) { + fun removeThis() { + if(isOnceListener) + handler.removeOnceListener(listener) + else + handler.removePersistentListener(listener) + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/MaxActiveContextsException.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/MaxActiveContextsException.kt index a5f45e26..306db052 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/MaxActiveContextsException.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/MaxActiveContextsException.kt @@ -1,3 +1,3 @@ -package com.github.serivesmejia.eocvsim.util.exception - +package com.github.serivesmejia.eocvsim.util.exception + class MaxActiveContextsException(message: String = "") : Exception(message) \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/CrashReport.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/CrashReport.kt index dbd38490..919973b7 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/CrashReport.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/CrashReport.kt @@ -1,118 +1,118 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.util.exception.handling - -import com.github.serivesmejia.eocvsim.EOCVSim -import com.github.serivesmejia.eocvsim.Build -import com.github.serivesmejia.eocvsim.util.Log -import com.github.serivesmejia.eocvsim.util.StrUtil -import com.github.serivesmejia.eocvsim.util.SysUtil -import com.github.serivesmejia.eocvsim.util.extension.plus -import java.io.File -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter - -class CrashReport(causedByException: Throwable) { - - private companion object { - val OS_ARCH = System.getProperty("os.arch") - val OS_VERSION = System.getProperty("os.version") - val OS_NAME = System.getProperty("os.name") - val SYSUTIL_DETECTED_OS = SysUtil.getOS() - val JAVA_VERSION = System.getProperty("java.version") - val JAVA_VENDOR = System.getProperty("java.vendor") - - val dtFormatter = DateTimeFormatter.ofPattern("yyyy_MM_dd-HH.mm.ss") - } - - private val sb = StringBuilder() - - init { - sb.appendLine("/--------------------------------\\").appendLine() - sb.appendLine(" EOCV-Sim v${EOCVSim.VERSION} crash report").appendLine() - sb.appendLine("\\--------------------------------/").appendLine() - - sb.appendLine(": Crash stacktrace").appendLine() - sb.appendLine(StrUtil.fromException(causedByException)).appendLine() - - sb.appendLine("==========================================").appendLine() - - sb.appendLine(": EOCV-Sim info") - sb.appendLine(" Version: ${EOCVSim.VERSION}") - sb.appendLine(" Built on: ${Build.buildDate}").appendLine() - - sb.appendLine(": System specs") - sb.appendLine(" OS name: $OS_NAME") - sb.appendLine(" OS version: $OS_VERSION") - sb.appendLine(" Detected OS: $SYSUTIL_DETECTED_OS") - sb.appendLine(" Architecture: $OS_ARCH") - sb.appendLine(" Java version: $JAVA_VERSION") - sb.appendLine(" Java vendor: $JAVA_VENDOR") - sb.appendLine(" Last memory usage: ${SysUtil.getMemoryUsageMB()} MB").appendLine() - - sb.appendLine("==========================================").appendLine() - - sb.appendLine(": Full thread dump").appendLine() - - for((thread, stacktrace) in Thread.getAllStackTraces()) { - sb.appendLine(" > Thread \"${thread.name}\"") - - for(element in stacktrace) { - sb.appendLine(" $element") - } - } - sb.appendLine() - - sb.appendLine("==================================").appendLine() - - sb.appendLine(": Full logs").appendLine() - sb.appendLine(Log.fullLogs.toString()).appendLine() - - sb.appendLine(";") - } - - fun saveCrashReport(f: File) { - SysUtil.saveFileStr(f, toString()) - Log.info("CrashReport", "Saved crash report to ${f.absolutePath}") - } - - fun saveCrashReport() { - val workingDir = File(System.getProperty("user.dir")) - val dateTimeStr = dtFormatter.format(LocalDateTime.now()) - - val crashLogFile = workingDir + "/crashreport-eocvsim-$dateTimeStr.log" - - saveCrashReport(crashLogFile) - } - - fun saveCrashReport(filename: String) { - val workingDir = File(System.getProperty("user.dir")) - val crashLogFile = workingDir + "/$filename.log" - - saveCrashReport(crashLogFile) - } - - override fun toString() = sb.toString() - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.util.exception.handling + +import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.Build +import com.github.serivesmejia.eocvsim.util.Log +import com.github.serivesmejia.eocvsim.util.StrUtil +import com.github.serivesmejia.eocvsim.util.SysUtil +import com.github.serivesmejia.eocvsim.util.extension.plus +import java.io.File +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +class CrashReport(causedByException: Throwable) { + + private companion object { + val OS_ARCH = System.getProperty("os.arch") + val OS_VERSION = System.getProperty("os.version") + val OS_NAME = System.getProperty("os.name") + val SYSUTIL_DETECTED_OS = SysUtil.getOS() + val JAVA_VERSION = System.getProperty("java.version") + val JAVA_VENDOR = System.getProperty("java.vendor") + + val dtFormatter = DateTimeFormatter.ofPattern("yyyy_MM_dd-HH.mm.ss") + } + + private val sb = StringBuilder() + + init { + sb.appendLine("/--------------------------------\\").appendLine() + sb.appendLine(" EOCV-Sim v${EOCVSim.VERSION} crash report").appendLine() + sb.appendLine("\\--------------------------------/").appendLine() + + sb.appendLine(": Crash stacktrace").appendLine() + sb.appendLine(StrUtil.fromException(causedByException)).appendLine() + + sb.appendLine("==========================================").appendLine() + + sb.appendLine(": EOCV-Sim info") + sb.appendLine(" Version: ${EOCVSim.VERSION}") + sb.appendLine(" Built on: ${Build.buildDate}").appendLine() + + sb.appendLine(": System specs") + sb.appendLine(" OS name: $OS_NAME") + sb.appendLine(" OS version: $OS_VERSION") + sb.appendLine(" Detected OS: $SYSUTIL_DETECTED_OS") + sb.appendLine(" Architecture: $OS_ARCH") + sb.appendLine(" Java version: $JAVA_VERSION") + sb.appendLine(" Java vendor: $JAVA_VENDOR") + sb.appendLine(" Last memory usage: ${SysUtil.getMemoryUsageMB()} MB").appendLine() + + sb.appendLine("==========================================").appendLine() + + sb.appendLine(": Full thread dump").appendLine() + + for((thread, stacktrace) in Thread.getAllStackTraces()) { + sb.appendLine(" > Thread \"${thread.name}\"") + + for(element in stacktrace) { + sb.appendLine(" $element") + } + } + sb.appendLine() + + sb.appendLine("==================================").appendLine() + + sb.appendLine(": Full logs").appendLine() + sb.appendLine(Log.fullLogs.toString()).appendLine() + + sb.appendLine(";") + } + + fun saveCrashReport(f: File) { + SysUtil.saveFileStr(f, toString()) + Log.info("CrashReport", "Saved crash report to ${f.absolutePath}") + } + + fun saveCrashReport() { + val workingDir = File(System.getProperty("user.dir")) + val dateTimeStr = dtFormatter.format(LocalDateTime.now()) + + val crashLogFile = workingDir + "/crashreport-eocvsim-$dateTimeStr.log" + + saveCrashReport(crashLogFile) + } + + fun saveCrashReport(filename: String) { + val workingDir = File(System.getProperty("user.dir")) + val crashLogFile = workingDir + "/$filename.log" + + saveCrashReport(crashLogFile) + } + + override fun toString() = sb.toString() + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/EOCVSimUncaughtExceptionHandler.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/EOCVSimUncaughtExceptionHandler.kt index 4451cd8a..ae87f2ac 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/EOCVSimUncaughtExceptionHandler.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/EOCVSimUncaughtExceptionHandler.kt @@ -1,57 +1,57 @@ -package com.github.serivesmejia.eocvsim.util.exception.handling - -import com.github.serivesmejia.eocvsim.currentMainThread -import com.github.serivesmejia.eocvsim.util.Log -import kotlin.system.exitProcess - -class EOCVSimUncaughtExceptionHandler private constructor() : Thread.UncaughtExceptionHandler { - - companion object { - const val MAX_UNCAUGHT_EXCEPTIONS_BEFORE_CRASH = 3 - - @JvmStatic fun register() { - Thread.setDefaultUncaughtExceptionHandler(EOCVSimUncaughtExceptionHandler()) - } - - private const val TAG = "EOCVSimUncaughtExceptionHandler" - } - - private var uncaughtExceptionsCount = 0 - - override fun uncaughtException(t: Thread, e: Throwable) { - //we don't want the whole app to crash on a simple interrupted exception right? - if(e is InterruptedException) { - Log.warn(TAG, "Uncaught InterruptedException thrown in Thread ${t.name}, it will be interrupted", e) - t.interrupt() - return - } - - uncaughtExceptionsCount++ - - Log.error(TAG,"Uncaught exception thrown in \"${t.name}\" thread", e) - Log.blank() - - //Exit if uncaught exception happened in the main thread - //since we would be basically in a deadlock state if that happened - //or if we have a lotta uncaught exceptions. - if(t == currentMainThread || uncaughtExceptionsCount > MAX_UNCAUGHT_EXCEPTIONS_BEFORE_CRASH) { - CrashReport(e).saveCrashReport() - - Log.warn(TAG, "If this error persists, open an issue on EOCV-Sim's GitHub attaching the crash report file.") - Log.blank() - Log.warn(TAG, "The application will exit now (exit code 1)") - - exitProcess(1) - } else { - CrashReport(e).saveCrashReport("lasterror-eocvsim") - - //if not, eocv sim might still be working (i.e a crash from a MatPoster thread) - //so we might not need to exit in this point, but we'll need to send a warning - //to the user - Log.warn(TAG, "If this error persists, open an issue on EOCV-Sim's GitHub.") - Log.blank() - Log.warn(TAG, "The application might not work as expected from this point") - } - } - +package com.github.serivesmejia.eocvsim.util.exception.handling + +import com.github.serivesmejia.eocvsim.currentMainThread +import com.github.serivesmejia.eocvsim.util.Log +import kotlin.system.exitProcess + +class EOCVSimUncaughtExceptionHandler private constructor() : Thread.UncaughtExceptionHandler { + + companion object { + const val MAX_UNCAUGHT_EXCEPTIONS_BEFORE_CRASH = 3 + + @JvmStatic fun register() { + Thread.setDefaultUncaughtExceptionHandler(EOCVSimUncaughtExceptionHandler()) + } + + private const val TAG = "EOCVSimUncaughtExceptionHandler" + } + + private var uncaughtExceptionsCount = 0 + + override fun uncaughtException(t: Thread, e: Throwable) { + //we don't want the whole app to crash on a simple interrupted exception right? + if(e is InterruptedException) { + Log.warn(TAG, "Uncaught InterruptedException thrown in Thread ${t.name}, it will be interrupted", e) + t.interrupt() + return + } + + uncaughtExceptionsCount++ + + Log.error(TAG,"Uncaught exception thrown in \"${t.name}\" thread", e) + Log.blank() + + //Exit if uncaught exception happened in the main thread + //since we would be basically in a deadlock state if that happened + //or if we have a lotta uncaught exceptions. + if(t == currentMainThread || uncaughtExceptionsCount > MAX_UNCAUGHT_EXCEPTIONS_BEFORE_CRASH) { + CrashReport(e).saveCrashReport() + + Log.warn(TAG, "If this error persists, open an issue on EOCV-Sim's GitHub attaching the crash report file.") + Log.blank() + Log.warn(TAG, "The application will exit now (exit code 1)") + + exitProcess(1) + } else { + CrashReport(e).saveCrashReport("lasterror-eocvsim") + + //if not, eocv sim might still be working (i.e a crash from a MatPoster thread) + //so we might not need to exit in this point, but we'll need to send a warning + //to the user + Log.warn(TAG, "If this error persists, open an issue on EOCV-Sim's GitHub.") + Log.blank() + Log.warn(TAG, "The application might not work as expected from this point") + } + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/CvExt.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/CvExt.kt index 7c4cc2b0..c192ca07 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/CvExt.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/CvExt.kt @@ -1,52 +1,52 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ -@file:JvmName("CvExt") - -package com.github.serivesmejia.eocvsim.util.extension - -import com.qualcomm.robotcore.util.Range -import org.opencv.core.CvType -import org.opencv.core.Mat -import org.opencv.core.Scalar -import org.opencv.core.Size -import org.opencv.imgproc.Imgproc - -fun Scalar.cvtColor(code: Int): Scalar { - val mat = Mat(5, 5, CvType.CV_8UC3); - mat.setTo(this) - Imgproc.cvtColor(mat, mat, code); - - val newScalar = Scalar(mat.get(1, 1)) - mat.release() - - return newScalar -} - -fun Size.aspectRatio() = height / width -fun Mat.aspectRatio() = size().aspectRatio() - -fun Size.clipTo(size: Size): Size { - width = Range.clip(width, 0.0, size.width) - height = Range.clip(height, 0.0, size.height) - return this +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +@file:JvmName("CvExt") + +package com.github.serivesmejia.eocvsim.util.extension + +import com.qualcomm.robotcore.util.Range +import org.opencv.core.CvType +import org.opencv.core.Mat +import org.opencv.core.Scalar +import org.opencv.core.Size +import org.opencv.imgproc.Imgproc + +fun Scalar.cvtColor(code: Int): Scalar { + val mat = Mat(5, 5, CvType.CV_8UC3); + mat.setTo(this) + Imgproc.cvtColor(mat, mat, code); + + val newScalar = Scalar(mat.get(1, 1)) + mat.release() + + return newScalar +} + +fun Size.aspectRatio() = height / width +fun Mat.aspectRatio() = size().aspectRatio() + +fun Size.clipTo(size: Size): Size { + width = Range.clip(width, 0.0, size.width) + height = Range.clip(height, 0.0, size.height) + return this } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/FileExt.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/FileExt.kt index 318bb7b7..c4f8e8a5 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/FileExt.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/FileExt.kt @@ -1,30 +1,30 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.util.extension - -import java.io.File - -operator fun File.plus(str: String): File { - return File(this.absolutePath + str) +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.util.extension + +import java.io.File + +operator fun File.plus(str: String): File { + return File(this.absolutePath + str) } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/NumberExt.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/NumberExt.kt index 8194aa84..0b83ed3f 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/NumberExt.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/NumberExt.kt @@ -1,34 +1,34 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.util.extension - -fun Int.clipUpperZero(): Int { - return if(this > 0) { - this - } else { - 0 - } -} - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.util.extension + +fun Int.clipUpperZero(): Int { + return if(this > 0) { + this + } else { + 0 + } +} + val Int.zeroBased get() = (this - 1).clipUpperZero() \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/StrExt.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/StrExt.kt index 10552992..b256c4f7 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/StrExt.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/extension/StrExt.kt @@ -1,31 +1,31 @@ -package com.github.serivesmejia.eocvsim.util.extension - -fun String.removeFromEnd(rem: String): String { - if(endsWith(rem)) { - return substring(0, length - rem.length).trim() - } - return trim() -} - -fun String.tabIndent() = replace("(?m)^", "\t") - -fun String.removeIndentFromFirstLine(): String { - val lines = split("\n") - - if(lines.size == 1) { - return lines[0].replace("\t", "") - } else { - var str = "" - for(i in 0..lines.size.zeroBased) { - val line = if(i == 0) { - lines[i].replace("\t", "") - } else { - lines[i] - } - - str += line + "\n" - } - - return str.trim() - } +package com.github.serivesmejia.eocvsim.util.extension + +fun String.removeFromEnd(rem: String): String { + if(endsWith(rem)) { + return substring(0, length - rem.length).trim() + } + return trim() +} + +fun String.tabIndent() = replace("(?m)^", "\t") + +fun String.removeIndentFromFirstLine(): String { + val lines = split("\n") + + if(lines.size == 1) { + return lines[0].replace("\t", "") + } else { + var str = "" + for(i in 0..lines.size.zeroBased) { + val line = if(i == 0) { + lines[i].replace("\t", "") + } else { + lines[i] + } + + str += line + "\n" + } + + return str.trim() + } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/fps/FpsCounter.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/fps/FpsCounter.kt index 511e8c97..0f422e99 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/fps/FpsCounter.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/fps/FpsCounter.kt @@ -1,58 +1,58 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.util.fps - -import com.qualcomm.robotcore.util.ElapsedTime -import com.qualcomm.robotcore.util.MovingStatistics - -class FpsCounter { - - private val elapsedTime = ElapsedTime() - - private val avgFpsStatistics = MovingStatistics(100) - - val avgFps: Double - get() { - return avgFpsStatistics.mean - } - - @Volatile - private var fpsCount = 0 - - @get:Synchronized - @Volatile - var fps = 0 - private set - - @Synchronized - fun update() { - fpsCount++ - if (elapsedTime.seconds() >= 1) { - fps = fpsCount; fpsCount = 0 - avgFpsStatistics.add(fps.toDouble()) - elapsedTime.reset() - } - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.util.fps + +import com.qualcomm.robotcore.util.ElapsedTime +import com.qualcomm.robotcore.util.MovingStatistics + +class FpsCounter { + + private val elapsedTime = ElapsedTime() + + private val avgFpsStatistics = MovingStatistics(100) + + val avgFps: Double + get() { + return avgFpsStatistics.mean + } + + @Volatile + private var fpsCount = 0 + + @get:Synchronized + @Volatile + var fps = 0 + private set + + @Synchronized + fun update() { + fpsCount++ + if (elapsedTime.seconds() >= 1) { + fps = fpsCount; fpsCount = 0 + avgFpsStatistics.add(fps.toDouble()) + elapsedTime.reset() + } + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/fps/FpsLimiter.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/fps/FpsLimiter.kt index 10fb7e25..1c36b284 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/fps/FpsLimiter.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/fps/FpsLimiter.kt @@ -1,42 +1,42 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.util.fps - -class FpsLimiter(var maxFPS: Double = 30.0) { - - @Volatile private var start = 0.0 - @Volatile private var diff = 0.0 - @Volatile private var wait = 0.0 - - @Throws(InterruptedException::class) - fun sync() { - wait = 1.0 / (maxFPS / 1000.0) - diff = System.currentTimeMillis() - start - if (diff < wait) { - Thread.sleep((wait - diff).toLong()) - } - start = System.currentTimeMillis().toDouble() - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.util.fps + +class FpsLimiter(var maxFPS: Double = 30.0) { + + @Volatile private var start = 0.0 + @Volatile private var diff = 0.0 + @Volatile private var wait = 0.0 + + @Throws(InterruptedException::class) + fun sync() { + wait = 1.0 / (maxFPS / 1000.0) + diff = System.currentTimeMillis() - start + if (diff < wait) { + Thread.sleep((wait - diff).toLong()) + } + start = System.currentTimeMillis().toDouble() + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/image/BufferedImageRecycler.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/image/BufferedImageRecycler.java index 77e41e8e..d2b21b3f 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/image/BufferedImageRecycler.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/image/BufferedImageRecycler.java @@ -1,107 +1,107 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.util.image; - -import java.awt.*; -import java.awt.image.BufferedImage; -import java.util.concurrent.ArrayBlockingQueue; - -public class BufferedImageRecycler { - - private final RecyclableBufferedImage[] allBufferedImages; - private final ArrayBlockingQueue availableBufferedImages; - - public BufferedImageRecycler(int num, int allImgWidth, int allImgHeight, int allImgType) { - allBufferedImages = new RecyclableBufferedImage[num]; - availableBufferedImages = new ArrayBlockingQueue<>(num); - - for (int i = 0; i < allBufferedImages.length; i++) { - allBufferedImages[i] = new RecyclableBufferedImage(i, allImgWidth, allImgHeight, allImgType); - availableBufferedImages.add(allBufferedImages[i]); - } - } - - public BufferedImageRecycler(int num, Dimension allImgSize, int allImgType) { - this(num, (int)allImgSize.getWidth(), (int)allImgSize.getHeight(), allImgType); - } - - public BufferedImageRecycler(int num, int allImgWidth, int allImgHeight) { - this(num, allImgWidth, allImgHeight, BufferedImage.TYPE_3BYTE_BGR); - } - - public BufferedImageRecycler(int num, Dimension allImgSize) { - this(num, (int)allImgSize.getWidth(), (int)allImgSize.getHeight(), BufferedImage.TYPE_3BYTE_BGR); - } - - public boolean isOnUse() { return allBufferedImages.length != availableBufferedImages.size(); } - - public synchronized RecyclableBufferedImage takeBufferedImage() { - - if (availableBufferedImages.size() == 0) { - throw new RuntimeException("All buffered images have been checked out!"); - } - - RecyclableBufferedImage buffImg = null; - try { - buffImg = availableBufferedImages.take(); - buffImg.checkedOut = true; - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - - return buffImg; - - } - - public synchronized void returnBufferedImage(RecyclableBufferedImage buffImg) { - if (buffImg != allBufferedImages[buffImg.idx]) { - throw new IllegalArgumentException("This BufferedImage does not belong to this recycler!"); - } - - if (buffImg.checkedOut) { - buffImg.checkedOut = false; - buffImg.flush(); - availableBufferedImages.add(buffImg); - } else { - throw new IllegalArgumentException("This BufferedImage has already been returned!"); - } - } - - public synchronized void flushAll() { - for(BufferedImage img : allBufferedImages) { - img.flush(); - } - } - - public static class RecyclableBufferedImage extends BufferedImage { - private int idx = -1; - private volatile boolean checkedOut = false; - - private RecyclableBufferedImage(int idx, int width, int height, int imageType) { - super(width, height, imageType); - this.idx = idx; - } - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.util.image; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.util.concurrent.ArrayBlockingQueue; + +public class BufferedImageRecycler { + + private final RecyclableBufferedImage[] allBufferedImages; + private final ArrayBlockingQueue availableBufferedImages; + + public BufferedImageRecycler(int num, int allImgWidth, int allImgHeight, int allImgType) { + allBufferedImages = new RecyclableBufferedImage[num]; + availableBufferedImages = new ArrayBlockingQueue<>(num); + + for (int i = 0; i < allBufferedImages.length; i++) { + allBufferedImages[i] = new RecyclableBufferedImage(i, allImgWidth, allImgHeight, allImgType); + availableBufferedImages.add(allBufferedImages[i]); + } + } + + public BufferedImageRecycler(int num, Dimension allImgSize, int allImgType) { + this(num, (int)allImgSize.getWidth(), (int)allImgSize.getHeight(), allImgType); + } + + public BufferedImageRecycler(int num, int allImgWidth, int allImgHeight) { + this(num, allImgWidth, allImgHeight, BufferedImage.TYPE_3BYTE_BGR); + } + + public BufferedImageRecycler(int num, Dimension allImgSize) { + this(num, (int)allImgSize.getWidth(), (int)allImgSize.getHeight(), BufferedImage.TYPE_3BYTE_BGR); + } + + public boolean isOnUse() { return allBufferedImages.length != availableBufferedImages.size(); } + + public synchronized RecyclableBufferedImage takeBufferedImage() { + + if (availableBufferedImages.size() == 0) { + throw new RuntimeException("All buffered images have been checked out!"); + } + + RecyclableBufferedImage buffImg = null; + try { + buffImg = availableBufferedImages.take(); + buffImg.checkedOut = true; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + return buffImg; + + } + + public synchronized void returnBufferedImage(RecyclableBufferedImage buffImg) { + if (buffImg != allBufferedImages[buffImg.idx]) { + throw new IllegalArgumentException("This BufferedImage does not belong to this recycler!"); + } + + if (buffImg.checkedOut) { + buffImg.checkedOut = false; + buffImg.flush(); + availableBufferedImages.add(buffImg); + } else { + throw new IllegalArgumentException("This BufferedImage has already been returned!"); + } + } + + public synchronized void flushAll() { + for(BufferedImage img : allBufferedImages) { + img.flush(); + } + } + + public static class RecyclableBufferedImage extends BufferedImage { + private int idx = -1; + private volatile boolean checkedOut = false; + + private RecyclableBufferedImage(int idx, int width, int height, int imageType) { + super(width, height, imageType); + this.idx = idx; + } + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/image/DynamicBufferedImageRecycler.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/image/DynamicBufferedImageRecycler.java index 941345d3..b4b50f3f 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/image/DynamicBufferedImageRecycler.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/image/DynamicBufferedImageRecycler.java @@ -1,78 +1,78 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.util.image; - -import java.awt.*; -import java.awt.image.BufferedImage; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - -public class DynamicBufferedImageRecycler { - - private final HashMap recyclers = new HashMap<>(); - - public synchronized BufferedImage giveBufferedImage(Dimension size, int recyclerSize) { - - //look for existing buff image recycler with desired dimensions - for(Map.Entry entry : recyclers.entrySet()) { - Dimension dimension = entry.getKey(); - BufferedImageRecycler recycler = entry.getValue(); - - if(dimension.equals(size)) { - BufferedImage buffImg = recycler.takeBufferedImage(); - buffImg.flush(); - return buffImg; - } else if(!recycler.isOnUse()) { - recycler.flushAll(); - recyclers.remove(dimension); - } - } - - //create new one if didn't found an existing recycler - BufferedImageRecycler recycler = new BufferedImageRecycler(recyclerSize, size); - recyclers.put(size, recycler); - - BufferedImage buffImg = recycler.takeBufferedImage(); - - return buffImg; - } - - public synchronized void returnBufferedImage(BufferedImage buffImg) { - Dimension dimension = new Dimension(buffImg.getWidth(), buffImg.getHeight()); - - BufferedImageRecycler recycler = recyclers.get(dimension); - - if(recycler != null) - recycler.returnBufferedImage((BufferedImageRecycler.RecyclableBufferedImage) buffImg); - } - - public synchronized void flushAll() { - for(BufferedImageRecycler recycler : recyclers.values()) { - recycler.flushAll(); - } - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.util.image; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class DynamicBufferedImageRecycler { + + private final HashMap recyclers = new HashMap<>(); + + public synchronized BufferedImage giveBufferedImage(Dimension size, int recyclerSize) { + + //look for existing buff image recycler with desired dimensions + for(Map.Entry entry : recyclers.entrySet()) { + Dimension dimension = entry.getKey(); + BufferedImageRecycler recycler = entry.getValue(); + + if(dimension.equals(size)) { + BufferedImage buffImg = recycler.takeBufferedImage(); + buffImg.flush(); + return buffImg; + } else if(!recycler.isOnUse()) { + recycler.flushAll(); + recyclers.remove(dimension); + } + } + + //create new one if didn't found an existing recycler + BufferedImageRecycler recycler = new BufferedImageRecycler(recyclerSize, size); + recyclers.put(size, recycler); + + BufferedImage buffImg = recycler.takeBufferedImage(); + + return buffImg; + } + + public synchronized void returnBufferedImage(BufferedImage buffImg) { + Dimension dimension = new Dimension(buffImg.getWidth(), buffImg.getHeight()); + + BufferedImageRecycler recycler = recyclers.get(dimension); + + if(recycler != null) + recycler.returnBufferedImage((BufferedImageRecycler.RecyclableBufferedImage) buffImg); + } + + public synchronized void flushAll() { + for(BufferedImageRecycler recycler : recyclers.values()) { + recycler.flushAll(); + } + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/io/EOCVSimFolder.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/io/EOCVSimFolder.kt index 2088e384..b9d42bba 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/io/EOCVSimFolder.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/io/EOCVSimFolder.kt @@ -1,16 +1,16 @@ -package com.github.serivesmejia.eocvsim.util.io - -import com.github.serivesmejia.eocvsim.util.SysUtil -import java.io.File - -object EOCVSimFolder : File(SysUtil.getAppData().absolutePath + separator + ".eocvsim") { - - val lock by lazy { lockDirectory() } - - val couldLock get() = lock != null && lock!!.isLocked - - init { - mkdir() - } - +package com.github.serivesmejia.eocvsim.util.io + +import com.github.serivesmejia.eocvsim.util.SysUtil +import java.io.File + +object EOCVSimFolder : File(SysUtil.getAppData().absolutePath + separator + ".eocvsim") { + + val lock by lazy { lockDirectory() } + + val couldLock get() = lock != null && lock!!.isLocked + + init { + mkdir() + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/io/FileWatcher.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/io/FileWatcher.kt index ae003668..232bb424 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/io/FileWatcher.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/io/FileWatcher.kt @@ -1,76 +1,76 @@ -package com.github.serivesmejia.eocvsim.util.io - -import com.github.serivesmejia.eocvsim.util.event.EventHandler -import com.github.serivesmejia.eocvsim.util.SysUtil -import com.github.serivesmejia.eocvsim.util.Log -import java.io.File -import java.util.* - -class FileWatcher(private val watchingDirectories: List, - watchingFileExtensions: List?, - name: String) { - - private val TAG = "FileWatcher-$name" - - val onChange = EventHandler("OnChange-$TAG") - - private val watcherThread = Thread( - Runner(watchingDirectories, watchingFileExtensions, onChange), - TAG - ) - - fun init() { - watcherThread.start() - } - - fun stop() { - watcherThread.interrupt() - } - - private class Runner(val watchingDirectories: List, - val fileExts: List?, - val onChange: EventHandler) : Runnable { - - private val lastModifyDates = mutableMapOf() - - override fun run() { - val TAG = Thread.currentThread().name!! - - val directoriesList = StringBuilder() - for(directory in watchingDirectories) { - directoriesList.appendLine(directory.absolutePath) - } - - Log.info(TAG, "Starting to watch directories in:\n$directoriesList") - - while(!Thread.currentThread().isInterrupted) { - var changeDetected = false - - for(directory in watchingDirectories) { - for(file in SysUtil.filesUnder(directory)) { - if(fileExts != null && !fileExts.stream().anyMatch { file.name.endsWith(".$it") }) - continue - - val path = file.absolutePath - val lastModified = file.lastModified() - - if(lastModifyDates.containsKey(path) && lastModified > lastModifyDates[path]!! && !changeDetected) { - Log.info(TAG, "Change detected on ${directory.absolutePath}") - - onChange.run() - changeDetected = true - } - - lastModifyDates[path] = lastModified - } - } - - Thread.sleep(1200) //check every 800 ms - } - - Log.info(TAG, "Stopping watching directories:\n$directoriesList") - } - - } - -} +package com.github.serivesmejia.eocvsim.util.io + +import com.github.serivesmejia.eocvsim.util.event.EventHandler +import com.github.serivesmejia.eocvsim.util.SysUtil +import com.github.serivesmejia.eocvsim.util.Log +import java.io.File +import java.util.* + +class FileWatcher(private val watchingDirectories: List, + watchingFileExtensions: List?, + name: String) { + + private val TAG = "FileWatcher-$name" + + val onChange = EventHandler("OnChange-$TAG") + + private val watcherThread = Thread( + Runner(watchingDirectories, watchingFileExtensions, onChange), + TAG + ) + + fun init() { + watcherThread.start() + } + + fun stop() { + watcherThread.interrupt() + } + + private class Runner(val watchingDirectories: List, + val fileExts: List?, + val onChange: EventHandler) : Runnable { + + private val lastModifyDates = mutableMapOf() + + override fun run() { + val TAG = Thread.currentThread().name!! + + val directoriesList = StringBuilder() + for(directory in watchingDirectories) { + directoriesList.appendLine(directory.absolutePath) + } + + Log.info(TAG, "Starting to watch directories in:\n$directoriesList") + + while(!Thread.currentThread().isInterrupted) { + var changeDetected = false + + for(directory in watchingDirectories) { + for(file in SysUtil.filesUnder(directory)) { + if(fileExts != null && !fileExts.stream().anyMatch { file.name.endsWith(".$it") }) + continue + + val path = file.absolutePath + val lastModified = file.lastModified() + + if(lastModifyDates.containsKey(path) && lastModified > lastModifyDates[path]!! && !changeDetected) { + Log.info(TAG, "Change detected on ${directory.absolutePath}") + + onChange.run() + changeDetected = true + } + + lastModifyDates[path] = lastModified + } + } + + Thread.sleep(1200) //check every 800 ms + } + + Log.info(TAG, "Stopping watching directories:\n$directoriesList") + } + + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/io/Lock.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/io/Lock.kt index 2b1c5ee4..fbd419a4 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/io/Lock.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/io/Lock.kt @@ -1,76 +1,76 @@ -package com.github.serivesmejia.eocvsim.util.io - -import com.github.serivesmejia.eocvsim.util.Log -import com.github.serivesmejia.eocvsim.util.SysUtil -import java.io.File -import java.io.RandomAccessFile -import java.nio.channels.FileLock - -class LockFile(pathname: String) : File(pathname) { - - private val raf by lazy { RandomAccessFile(this, "rw") } - - var lock: FileLock? = null - private set - - val isLocked get() = try { - raf - if(lock != null) !tryLock(false) else false - } catch(ex: Exception) { - Log.warn(TAG, "Can't open lock file $absolutePath") - true - } - - companion object { - const val TAG = "LockFile" - } - - init { - if(isDirectory) - throw IllegalArgumentException("Lock file cannot be a directory") - - if(!exists()) - SysUtil.saveFileStr(this, "") - } - - fun tryLock(log: Boolean = true): Boolean { - return try { - lock = raf.channel.tryLock() - if(log) Log.info(TAG, "Probably locked file $absolutePath") - true - } catch(ex: Exception) { - if(log) Log.warn(TAG, "Couldn't lock file $absolutePath", ex); - false - } - } - - fun unlock() { - lock?.release() - raf.close() - - lock = null - } - -} - -val File.directoryLockFile get() = LockFile(absolutePath + File.separator + ".lock") - -val File.isDirectoryLocked: Boolean get() { - val lock = directoryLockFile - val isLocked = lock.isLocked - - lock.unlock() - return isLocked -} - -fun File.lockDirectory(): LockFile? { - if(!isDirectory) - return null - - val lockFile = directoryLockFile - - if(isDirectoryLocked || !lockFile.tryLock()) - return null - - return lockFile +package com.github.serivesmejia.eocvsim.util.io + +import com.github.serivesmejia.eocvsim.util.Log +import com.github.serivesmejia.eocvsim.util.SysUtil +import java.io.File +import java.io.RandomAccessFile +import java.nio.channels.FileLock + +class LockFile(pathname: String) : File(pathname) { + + private val raf by lazy { RandomAccessFile(this, "rw") } + + var lock: FileLock? = null + private set + + val isLocked get() = try { + raf + if(lock != null) !tryLock(false) else false + } catch(ex: Exception) { + Log.warn(TAG, "Can't open lock file $absolutePath") + true + } + + companion object { + const val TAG = "LockFile" + } + + init { + if(isDirectory) + throw IllegalArgumentException("Lock file cannot be a directory") + + if(!exists()) + SysUtil.saveFileStr(this, "") + } + + fun tryLock(log: Boolean = true): Boolean { + return try { + lock = raf.channel.tryLock() + if(log) Log.info(TAG, "Probably locked file $absolutePath") + true + } catch(ex: Exception) { + if(log) Log.warn(TAG, "Couldn't lock file $absolutePath", ex); + false + } + } + + fun unlock() { + lock?.release() + raf.close() + + lock = null + } + +} + +val File.directoryLockFile get() = LockFile(absolutePath + File.separator + ".lock") + +val File.isDirectoryLocked: Boolean get() { + val lock = directoryLockFile + val isLocked = lock.isLocked + + lock.unlock() + return isLocked +} + +fun File.lockDirectory(): LockFile? { + if(!isDirectory) + return null + + val lockFile = directoryLockFile + + if(isDirectoryLocked || !lockFile.tryLock()) + return null + + return lockFile } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/WorkspaceManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/WorkspaceManager.kt index dd6ec5c2..9517ecea 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/WorkspaceManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/WorkspaceManager.kt @@ -1,191 +1,191 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.workspace - -import com.github.serivesmejia.eocvsim.EOCVSim -import com.github.serivesmejia.eocvsim.pipeline.compiler.CompiledPipelineManager -import com.github.serivesmejia.eocvsim.util.event.EventHandler -import com.github.serivesmejia.eocvsim.util.io.FileWatcher -import com.github.serivesmejia.eocvsim.util.Log -import com.github.serivesmejia.eocvsim.util.SysUtil -import com.github.serivesmejia.eocvsim.workspace.config.WorkspaceConfig -import com.github.serivesmejia.eocvsim.workspace.config.WorkspaceConfigLoader -import com.github.serivesmejia.eocvsim.workspace.util.WorkspaceTemplate -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import java.io.File -import java.nio.file.Paths - -class WorkspaceManager(val eocvSim: EOCVSim) { - - companion object { - private const val TAG = "WorkspaceManager" - } - - val workspaceConfigLoader by lazy { WorkspaceConfigLoader(workspaceFile) } - - var workspaceFile = File(".") - set(value) { - if(value != workspaceFile) { - workspaceConfigLoader.workspaceFile = value - - eocvSim.config.workspacePath = value.absolutePath - eocvSim.configManager.saveToFile() - - field = value - - Log.info(TAG, "Set current workspace to ${value.absolutePath}") - - if(::fileWatcher.isInitialized) - fileWatcher.stop() - - fileWatcher = FileWatcher( - arrayListOf( - sourcesAbsolutePath.toFile(), - resourcesAbsolutePath.toFile() - ), null, "Workspace" - ) - fileWatcher.init() - - onWorkspaceChange.run() - } - - cachedWorkspConfig = workspaceConfigLoader.loadWorkspaceConfig() - - if(cachedWorkspConfig == null) { - cachedWorkspConfig = WorkspaceConfig() - - if(workspaceConfigLoader.workspaceConfigFile.exists()) - Log.warn(TAG, "Recreating workspace config file, old one failed to parse") - else - Log.info(TAG, "Creating workspace config file...") - - workspaceConfigLoader.saveWorkspaceConfig(workspaceConfig) - } else { - Log.info(TAG, "Loaded workspace config successfully") - } - } - - private var cachedWorkspConfig: WorkspaceConfig? = null - - var workspaceConfig: WorkspaceConfig - set(value) { - Log.info(TAG, "Saving workspace config file of ${workspaceFile.absolutePath}") - workspaceConfigLoader.saveWorkspaceConfig(value) - cachedWorkspConfig = value - } - get() { - if(cachedWorkspConfig == null) - ::workspaceFile.set(workspaceFile) - - return cachedWorkspConfig!! - } - - val sourcesRelativePath get() = workspaceConfig.sourcesPath!! - val sourcesAbsolutePath get() = Paths.get(workspaceFile.absolutePath, sourcesRelativePath).normalize()!! - - val resourcesRelativePath get() = workspaceConfig.resourcesPath!! - val resourcesAbsolutePath get() = Paths.get(workspaceFile.absolutePath, resourcesRelativePath).normalize()!! - - val excludedRelativePaths get() = workspaceConfig.excludedPaths - val excludedAbsolutePaths get() = excludedRelativePaths.map { - Paths.get(workspaceFile.absolutePath, it).normalize()!! - } - - val excludedFileExtensions get() = workspaceConfig.excludedFileExtensions - - // TODO: Excluding ignored paths - val sourceFiles get() = SysUtil.filesUnder(sourcesAbsolutePath.toFile()) { file -> - file.name.endsWith(".java") && excludedAbsolutePaths.stream().noneMatch { - file.startsWith(it.toFile().absolutePath) - } - } - - val resourceFiles get() = SysUtil.filesUnder(resourcesAbsolutePath.toFile()) { file -> - file.name.run { - !endsWith(".java") && !endsWith(".class") && this != "eocvsim_workspace.json" - } && excludedAbsolutePaths.stream().noneMatch { - file.startsWith(it.toFile().absolutePath) - } && excludedFileExtensions.stream().noneMatch { - file.name.endsWith(".$it") - } - } - - val onWorkspaceChange = EventHandler("WorkspaceManager-OnChange") - - lateinit var fileWatcher: FileWatcher - private set - - fun stopFileWatcher() { - if(::fileWatcher.isInitialized) { - fileWatcher.stop() - } - } - - fun createWorkspaceWithTemplate(folder: File, template: WorkspaceTemplate): Boolean { - if(!folder.isDirectory) return false - if(!template.extractToIfEmpty(folder)) return false - - workspaceFile = folder - return true - } - - fun createWorkspaceWithTemplateAsync(folder: File, template: WorkspaceTemplate) = GlobalScope.launch(Dispatchers.IO) { - if(!folder.isDirectory) return@launch - if(!template.extractToIfEmpty(folder)) return@launch - - eocvSim.onMainUpdate.doOnce { - workspaceFile = folder - eocvSim.visualizer.asyncCompilePipelines() - } - } - - fun init() { - onWorkspaceChange { - fileWatcher.onChange { - eocvSim.pipelineManager.compiledPipelineManager.asyncCompile() - } - } - - val file = eocvSim.params.initialWorkspace ?: File(eocvSim.config.workspacePath) - - workspaceFile = if(file.exists()) - file - else - CompiledPipelineManager.DEF_WORKSPACE_FOLDER - - Log.blank() - } - - fun saveCurrentConfig() { - ::workspaceConfig.set(workspaceConfig) - } - - fun reloadConfig(): WorkspaceConfig { - cachedWorkspConfig = null - return workspaceConfig - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.workspace + +import com.github.serivesmejia.eocvsim.EOCVSim +import com.github.serivesmejia.eocvsim.pipeline.compiler.CompiledPipelineManager +import com.github.serivesmejia.eocvsim.util.event.EventHandler +import com.github.serivesmejia.eocvsim.util.io.FileWatcher +import com.github.serivesmejia.eocvsim.util.Log +import com.github.serivesmejia.eocvsim.util.SysUtil +import com.github.serivesmejia.eocvsim.workspace.config.WorkspaceConfig +import com.github.serivesmejia.eocvsim.workspace.config.WorkspaceConfigLoader +import com.github.serivesmejia.eocvsim.workspace.util.WorkspaceTemplate +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import java.io.File +import java.nio.file.Paths + +class WorkspaceManager(val eocvSim: EOCVSim) { + + companion object { + private const val TAG = "WorkspaceManager" + } + + val workspaceConfigLoader by lazy { WorkspaceConfigLoader(workspaceFile) } + + var workspaceFile = File(".") + set(value) { + if(value != workspaceFile) { + workspaceConfigLoader.workspaceFile = value + + eocvSim.config.workspacePath = value.absolutePath + eocvSim.configManager.saveToFile() + + field = value + + Log.info(TAG, "Set current workspace to ${value.absolutePath}") + + if(::fileWatcher.isInitialized) + fileWatcher.stop() + + fileWatcher = FileWatcher( + arrayListOf( + sourcesAbsolutePath.toFile(), + resourcesAbsolutePath.toFile() + ), null, "Workspace" + ) + fileWatcher.init() + + onWorkspaceChange.run() + } + + cachedWorkspConfig = workspaceConfigLoader.loadWorkspaceConfig() + + if(cachedWorkspConfig == null) { + cachedWorkspConfig = WorkspaceConfig() + + if(workspaceConfigLoader.workspaceConfigFile.exists()) + Log.warn(TAG, "Recreating workspace config file, old one failed to parse") + else + Log.info(TAG, "Creating workspace config file...") + + workspaceConfigLoader.saveWorkspaceConfig(workspaceConfig) + } else { + Log.info(TAG, "Loaded workspace config successfully") + } + } + + private var cachedWorkspConfig: WorkspaceConfig? = null + + var workspaceConfig: WorkspaceConfig + set(value) { + Log.info(TAG, "Saving workspace config file of ${workspaceFile.absolutePath}") + workspaceConfigLoader.saveWorkspaceConfig(value) + cachedWorkspConfig = value + } + get() { + if(cachedWorkspConfig == null) + ::workspaceFile.set(workspaceFile) + + return cachedWorkspConfig!! + } + + val sourcesRelativePath get() = workspaceConfig.sourcesPath!! + val sourcesAbsolutePath get() = Paths.get(workspaceFile.absolutePath, sourcesRelativePath).normalize()!! + + val resourcesRelativePath get() = workspaceConfig.resourcesPath!! + val resourcesAbsolutePath get() = Paths.get(workspaceFile.absolutePath, resourcesRelativePath).normalize()!! + + val excludedRelativePaths get() = workspaceConfig.excludedPaths + val excludedAbsolutePaths get() = excludedRelativePaths.map { + Paths.get(workspaceFile.absolutePath, it).normalize()!! + } + + val excludedFileExtensions get() = workspaceConfig.excludedFileExtensions + + // TODO: Excluding ignored paths + val sourceFiles get() = SysUtil.filesUnder(sourcesAbsolutePath.toFile()) { file -> + file.name.endsWith(".java") && excludedAbsolutePaths.stream().noneMatch { + file.startsWith(it.toFile().absolutePath) + } + } + + val resourceFiles get() = SysUtil.filesUnder(resourcesAbsolutePath.toFile()) { file -> + file.name.run { + !endsWith(".java") && !endsWith(".class") && this != "eocvsim_workspace.json" + } && excludedAbsolutePaths.stream().noneMatch { + file.startsWith(it.toFile().absolutePath) + } && excludedFileExtensions.stream().noneMatch { + file.name.endsWith(".$it") + } + } + + val onWorkspaceChange = EventHandler("WorkspaceManager-OnChange") + + lateinit var fileWatcher: FileWatcher + private set + + fun stopFileWatcher() { + if(::fileWatcher.isInitialized) { + fileWatcher.stop() + } + } + + fun createWorkspaceWithTemplate(folder: File, template: WorkspaceTemplate): Boolean { + if(!folder.isDirectory) return false + if(!template.extractToIfEmpty(folder)) return false + + workspaceFile = folder + return true + } + + fun createWorkspaceWithTemplateAsync(folder: File, template: WorkspaceTemplate) = GlobalScope.launch(Dispatchers.IO) { + if(!folder.isDirectory) return@launch + if(!template.extractToIfEmpty(folder)) return@launch + + eocvSim.onMainUpdate.doOnce { + workspaceFile = folder + eocvSim.visualizer.asyncCompilePipelines() + } + } + + fun init() { + onWorkspaceChange { + fileWatcher.onChange { + eocvSim.pipelineManager.compiledPipelineManager.asyncCompile() + } + } + + val file = eocvSim.params.initialWorkspace ?: File(eocvSim.config.workspacePath) + + workspaceFile = if(file.exists()) + file + else + CompiledPipelineManager.DEF_WORKSPACE_FOLDER + + Log.blank() + } + + fun saveCurrentConfig() { + ::workspaceConfig.set(workspaceConfig) + } + + fun reloadConfig(): WorkspaceConfig { + cachedWorkspConfig = null + return workspaceConfig + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfig.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfig.java index 061abb8f..1423855e 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfig.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfig.java @@ -1,36 +1,36 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.workspace.config; - -import java.util.ArrayList; - -public class WorkspaceConfig { - - public String sourcesPath = "."; - public String resourcesPath = "."; - - public ArrayList excludedPaths = new ArrayList<>(); - public ArrayList excludedFileExtensions = new ArrayList<>(); - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.workspace.config; + +import java.util.ArrayList; + +public class WorkspaceConfig { + + public String sourcesPath = "."; + public String resourcesPath = "."; + + public ArrayList excludedPaths = new ArrayList<>(); + public ArrayList excludedFileExtensions = new ArrayList<>(); + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfigLoader.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfigLoader.kt index 3a78de50..05ac32a7 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfigLoader.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/config/WorkspaceConfigLoader.kt @@ -1,32 +1,32 @@ -package com.github.serivesmejia.eocvsim.workspace.config - -import com.github.serivesmejia.eocvsim.util.SysUtil -import com.google.gson.GsonBuilder -import java.io.File - -class WorkspaceConfigLoader(var workspaceFile: File) { - - companion object { - private val gson = GsonBuilder().setPrettyPrinting().create() - } - - val workspaceConfigFile get() = File(workspaceFile, File.separator + "eocvsim_workspace.json") - - fun loadWorkspaceConfig(): WorkspaceConfig? { - if(!workspaceConfigFile.exists()) return null - - val configStr = SysUtil.loadFileStr(workspaceConfigFile) - - return try { - gson.fromJson(configStr, WorkspaceConfig::class.java) - } catch(e: Exception) { - null - } - } - - fun saveWorkspaceConfig(config: WorkspaceConfig) { - val configStr = gson.toJson(config) - SysUtil.saveFileStr(workspaceConfigFile, configStr) - } - +package com.github.serivesmejia.eocvsim.workspace.config + +import com.github.serivesmejia.eocvsim.util.SysUtil +import com.google.gson.GsonBuilder +import java.io.File + +class WorkspaceConfigLoader(var workspaceFile: File) { + + companion object { + private val gson = GsonBuilder().setPrettyPrinting().create() + } + + val workspaceConfigFile get() = File(workspaceFile, File.separator + "eocvsim_workspace.json") + + fun loadWorkspaceConfig(): WorkspaceConfig? { + if(!workspaceConfigFile.exists()) return null + + val configStr = SysUtil.loadFileStr(workspaceConfigFile) + + return try { + gson.fromJson(configStr, WorkspaceConfig::class.java) + } catch(e: Exception) { + null + } + } + + fun saveWorkspaceConfig(config: WorkspaceConfig) { + val configStr = gson.toJson(config) + SysUtil.saveFileStr(workspaceConfigFile, configStr) + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/VSCodeLauncher.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/VSCodeLauncher.kt index 9ff44061..06f6a8ff 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/VSCodeLauncher.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/VSCodeLauncher.kt @@ -1,53 +1,53 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.workspace.util - -import com.github.serivesmejia.eocvsim.util.Log -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch - -import com.github.serivesmejia.eocvsim.util.SysUtil -import java.io.File - -object VSCodeLauncher { - - private val TAG = "VSCodeLauncher" - - fun launch(workspace: File) { - Log.info(TAG, "Opening VS Code...") - - val result = SysUtil.runShellCommand("code \"${workspace.absolutePath}\"") - - if(result.output.isNotEmpty()) Log.info(TAG, result.output) - - if(result.exitCode == 0) - Log.info(TAG, "VS Code opened") - else - Log.info(TAG, "VS Code failed to open") - } - - fun asyncLaunch(workspace: File) = GlobalScope.launch(Dispatchers.IO) { launch(workspace) } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.workspace.util + +import com.github.serivesmejia.eocvsim.util.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + +import com.github.serivesmejia.eocvsim.util.SysUtil +import java.io.File + +object VSCodeLauncher { + + private val TAG = "VSCodeLauncher" + + fun launch(workspace: File) { + Log.info(TAG, "Opening VS Code...") + + val result = SysUtil.runShellCommand("code \"${workspace.absolutePath}\"") + + if(result.output.isNotEmpty()) Log.info(TAG, result.output) + + if(result.exitCode == 0) + Log.info(TAG, "VS Code opened") + else + Log.info(TAG, "VS Code failed to open") + } + + fun asyncLaunch(workspace: File) = GlobalScope.launch(Dispatchers.IO) { launch(workspace) } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/WorkspaceTemplate.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/WorkspaceTemplate.kt index a0f60d83..d8fe3cb6 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/WorkspaceTemplate.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/WorkspaceTemplate.kt @@ -1,40 +1,40 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.workspace.util - -import java.io.File - -abstract class WorkspaceTemplate { - - fun extractToIfEmpty(folder: File): Boolean { - if(folder.isDirectory && folder.listFiles()!!.isEmpty()) { - return extractTo(folder) - } - - return false - } - - abstract fun extractTo(folder: File): Boolean - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.workspace.util + +import java.io.File + +abstract class WorkspaceTemplate { + + fun extractToIfEmpty(folder: File): Boolean { + if(folder.isDirectory && folder.listFiles()!!.isEmpty()) { + return extractTo(folder) + } + + return false + } + + abstract fun extractTo(folder: File): Boolean + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/template/DefaultWorkspaceTemplate.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/template/DefaultWorkspaceTemplate.kt index dd7de8ad..9626d7ca 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/template/DefaultWorkspaceTemplate.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/template/DefaultWorkspaceTemplate.kt @@ -1,60 +1,60 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.workspace.util.template - -import com.github.serivesmejia.eocvsim.util.Log -import com.github.serivesmejia.eocvsim.util.SysUtil -import com.github.serivesmejia.eocvsim.workspace.util.VSCodeLauncher -import com.github.serivesmejia.eocvsim.workspace.util.WorkspaceTemplate -import net.lingala.zip4j.ZipFile -import java.io.File -import java.io.IOException - -object DefaultWorkspaceTemplate : WorkspaceTemplate() { - - private val TAG = "DefaultWorkspaceTemplate" - - val templateZipResource = javaClass.getResourceAsStream("/templates/default_workspace.zip") - - override fun extractTo(folder: File): Boolean { - if(!folder.isDirectory) return false - - val templateZipFile = SysUtil.copyFileIsTemp( - templateZipResource, "default_workspace.zip", false - ).file - - return try { - Log.info(TAG, "Extracting template to ${folder.absolutePath}") - - ZipFile(templateZipFile).extractAll(folder.absolutePath) - - Log.info(TAG, "Successfully extracted template") - true - } catch(ex: IOException) { - Log.warn(TAG, "Failed to extract workspace template to ${folder.absolutePath}", ex) - false - } - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.workspace.util.template + +import com.github.serivesmejia.eocvsim.util.Log +import com.github.serivesmejia.eocvsim.util.SysUtil +import com.github.serivesmejia.eocvsim.workspace.util.VSCodeLauncher +import com.github.serivesmejia.eocvsim.workspace.util.WorkspaceTemplate +import net.lingala.zip4j.ZipFile +import java.io.File +import java.io.IOException + +object DefaultWorkspaceTemplate : WorkspaceTemplate() { + + private val TAG = "DefaultWorkspaceTemplate" + + val templateZipResource = javaClass.getResourceAsStream("/templates/default_workspace.zip") + + override fun extractTo(folder: File): Boolean { + if(!folder.isDirectory) return false + + val templateZipFile = SysUtil.copyFileIsTemp( + templateZipResource, "default_workspace.zip", false + ).file + + return try { + Log.info(TAG, "Extracting template to ${folder.absolutePath}") + + ZipFile(templateZipFile).extractAll(folder.absolutePath) + + Log.info(TAG, "Successfully extracted template") + true + } catch(ex: IOException) { + Log.warn(TAG, "Failed to extract workspace template to ${folder.absolutePath}", ex) + false + } + } + +} diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/template/GradleWorkspaceTemplate.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/template/GradleWorkspaceTemplate.kt index 72c16827..4c883b1e 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/template/GradleWorkspaceTemplate.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/template/GradleWorkspaceTemplate.kt @@ -1,76 +1,76 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package com.github.serivesmejia.eocvsim.workspace.util.template - -import com.github.serivesmejia.eocvsim.util.Log -import com.github.serivesmejia.eocvsim.Build -import com.github.serivesmejia.eocvsim.util.SysUtil -import com.github.serivesmejia.eocvsim.workspace.util.VSCodeLauncher -import com.github.serivesmejia.eocvsim.workspace.util.WorkspaceTemplate -import com.github.serivesmejia.eocvsim.EOCVSim -import net.lingala.zip4j.ZipFile -import java.io.File -import java.io.IOException - -object GradleWorkspaceTemplate : WorkspaceTemplate() { - - private val TAG = "GradleWorkspaceTemplate" - - val templateZipResource = javaClass.getResourceAsStream("/templates/gradle_workspace.zip") - - override fun extractTo(folder: File): Boolean { - if(!folder.isDirectory) return false - - val templateZipFile = SysUtil.copyFileIsTemp( - templateZipResource, "gradle_workspace.zip", false - ).file - - return try { - Log.info(TAG, "Extracting template to ${folder.absolutePath}") - - ZipFile(templateZipFile).extractAll(folder.absolutePath) - - Log.info(TAG, "Successfully extracted template") - reformatTemplate(folder) //format necessary template files in the folder - - VSCodeLauncher.asyncLaunch(folder) // launch vs code - true - } catch(ex: IOException) { - Log.warn(TAG, "Failed to extract workspace template to ${folder.absolutePath}", ex) - false - } - } - - private fun reformatTemplate(folder: File) { - val settingsGradleFile = File(folder, File.separator + "settings.gradle") - val buildGradleFile = File(folder, File.separator + "build.gradle") - - //replace the root project name variable in the file to the root folder name - SysUtil.replaceStrInFile(settingsGradleFile, "\$workspace_name", folder.name) - - //replace the version of the eocvsim dependency in build.gradle to the current one - SysUtil.replaceStrInFile(buildGradleFile, "\$version", Build.standardVersionString) - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.github.serivesmejia.eocvsim.workspace.util.template + +import com.github.serivesmejia.eocvsim.util.Log +import com.github.serivesmejia.eocvsim.Build +import com.github.serivesmejia.eocvsim.util.SysUtil +import com.github.serivesmejia.eocvsim.workspace.util.VSCodeLauncher +import com.github.serivesmejia.eocvsim.workspace.util.WorkspaceTemplate +import com.github.serivesmejia.eocvsim.EOCVSim +import net.lingala.zip4j.ZipFile +import java.io.File +import java.io.IOException + +object GradleWorkspaceTemplate : WorkspaceTemplate() { + + private val TAG = "GradleWorkspaceTemplate" + + val templateZipResource = javaClass.getResourceAsStream("/templates/gradle_workspace.zip") + + override fun extractTo(folder: File): Boolean { + if(!folder.isDirectory) return false + + val templateZipFile = SysUtil.copyFileIsTemp( + templateZipResource, "gradle_workspace.zip", false + ).file + + return try { + Log.info(TAG, "Extracting template to ${folder.absolutePath}") + + ZipFile(templateZipFile).extractAll(folder.absolutePath) + + Log.info(TAG, "Successfully extracted template") + reformatTemplate(folder) //format necessary template files in the folder + + VSCodeLauncher.asyncLaunch(folder) // launch vs code + true + } catch(ex: IOException) { + Log.warn(TAG, "Failed to extract workspace template to ${folder.absolutePath}", ex) + false + } + } + + private fun reformatTemplate(folder: File) { + val settingsGradleFile = File(folder, File.separator + "settings.gradle") + val buildGradleFile = File(folder, File.separator + "build.gradle") + + //replace the root project name variable in the file to the root folder name + SysUtil.replaceStrInFile(settingsGradleFile, "\$workspace_name", folder.name) + + //replace the version of the eocvsim dependency in build.gradle to the current one + SysUtil.replaceStrInFile(buildGradleFile, "\$version", Build.standardVersionString) + } + +} diff --git a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/util/ElapsedTime.java b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/util/ElapsedTime.java index bd216dcd..a508fd5c 100644 --- a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/util/ElapsedTime.java +++ b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/util/ElapsedTime.java @@ -1,266 +1,266 @@ -/* Copyright (c) 2014, 2015 Qualcomm Technologies Inc -All rights reserved. -Redistribution and use in source and binary forms, with or without modification, -are permitted (subject to the limitations in the disclaimer below) provided that -the following conditions are met: -Redistributions of source code must retain the above copyright notice, this list -of conditions and the following disclaimer. -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. -Neither the name of Qualcomm Technologies Inc nor the names of its contributors -may be used to endorse or promote products derived from this software without -specific prior written permission. -NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS -LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ - -package com.qualcomm.robotcore.util; - -import com.github.serivesmejia.eocvsim.util.Log; - -import java.util.concurrent.TimeUnit; - -/** - * The {@link ElapsedTime} class provides a simple handy timer to measure elapsed time intervals. - * The timer does not provide events or callbacks, as some other timers do. Rather, at an application- - * determined juncture, one can {@link #reset()} the timer. Thereafter, one can query the interval - * of wall-clock time that has subsequently elapsed by calling the {@link #time()}, {@link #seconds()}, - * or {@link #milliseconds()} methods. The timer has nanosecond internal accuracy. The precision - * reported by the {@link #time()} method is either seconds or milliseconds, depending on how the - * timer is initially constructed. - *

- * This class is thread-safe. - */ -@SuppressWarnings("WeakerAccess") -public class ElapsedTime { - - //------------------------------------------------------------------------------------------------ - // Types and constants - //------------------------------------------------------------------------------------------------ - - /** - * the number of nanoseconds in a second - */ - public static final long SECOND_IN_NANO = 1000000000; - /** - * the number of nanoseconds in a millisecond - */ - public static final long MILLIS_IN_NANO = 1000000; - protected final double resolution; - - //------------------------------------------------------------------------------------------------ - // State - //------------------------------------------------------------------------------------------------ - protected volatile long nsStartTime; - /** - * Creates a timer with resolution {@link com.qualcomm.robotcore.util.ElapsedTime.Resolution#SECONDS Resolution.Seconds} - * that is initialized with the now-current time. - * - * @see #ElapsedTime(long) - * @see #ElapsedTime(Resolution) - */ - public ElapsedTime() { - reset(); - this.resolution = SECOND_IN_NANO; - } - - //------------------------------------------------------------------------------------------------ - // Construction - //------------------------------------------------------------------------------------------------ - - /** - * Creates a timer with resolution {@link com.qualcomm.robotcore.util.ElapsedTime.Resolution#SECONDS Resolution.Seconds}. - * The timer is initialized with the provided start time. Zero is often a useful value to provide - * here: in common usage such timers will often be processed by application logic virtually immediately. - * - * @param startTime the initial value of the timer - * @see #ElapsedTime() - */ - public ElapsedTime(long startTime) { - this.nsStartTime = startTime; - this.resolution = SECOND_IN_NANO; - } - - /** - * Creates a timer with a resolution of seconds or milliseconds. The resolution - * affects the units in which the {@link #time()} method reports. The timer is initialized - * with the current time. - * - * @param resolution the resolution of the new timer - * @see #ElapsedTime() - */ - public ElapsedTime(Resolution resolution) { - reset(); - switch (resolution) { - case SECONDS: - default: - this.resolution = SECOND_IN_NANO; - break; - case MILLISECONDS: - this.resolution = MILLIS_IN_NANO; - break; - } - } - - protected long nsNow() { - return System.nanoTime(); - } - - //------------------------------------------------------------------------------------------------ - // Operations - //------------------------------------------------------------------------------------------------ - - /** - * Returns the current time on the clock used by the timer - * - * @param unit the time unit in which the current time should be returned - * @return the current time on the clock used by the timer - */ - public long now(TimeUnit unit) { - return unit.convert(nsNow(), TimeUnit.NANOSECONDS); - } - - /** - * Resets the internal state of the timer to reflect the current time. Instantaneously following - * this reset, {@link #time()} will report as zero. - * - * @see #time() - */ - public void reset() { - nsStartTime = nsNow(); - } - - /** - * Returns, in resolution-dependent units, the time at which this timer was last reset. - * - * @return the reset time of the timer - */ - public double startTime() { - return nsStartTime / resolution; - } - - /** - * Returns the time at which the timer was last reset, in units of nanoseconds - * - * @return the time at which the timer was last reset, in units of nanoseconds - */ - public long startTimeNanoseconds() { - return this.nsStartTime; - } - - /** - * Returns the duration that has elapsed since the last reset of this timer. - * Units used are either seconds or milliseconds, depending on the resolution with - * which the timer was instantiated. - * - * @return time duration since last timer reset - * @see #ElapsedTime() - * @see #ElapsedTime(Resolution) - * @see #seconds() - * @see #milliseconds() - */ - public double time() { - return (nsNow() - nsStartTime) / resolution; - } - - /** - * Returns the duration that has elapsed since the last reset of this timer - * as an integer in the units requested. - * - * @param unit the units in which to return the answer - * @return time duration since last timer reset - */ - public long time(TimeUnit unit) { - return unit.convert(nanoseconds(), TimeUnit.NANOSECONDS); - } - - /** - * Returns the duration that has elapsed since the last reset of this timer in seconds - * - * @return time duration since last timer reset - * @see #time() - */ - public double seconds() { - return nanoseconds() / ((double) (SECOND_IN_NANO)); - } - - /** - * Returns the duration that has elapsed since the last reset of this timer in milliseconds - * - * @return time duration since last timer reset - * @see #time() - */ - public double milliseconds() { - return seconds() * 1000; - } - - /** - * Returns the duration that has elapsed since the last reset of this timer in nanoseconds - * - * @return time duration since last timer reset - * @see #time() - */ - public long nanoseconds() { - return (nsNow() - nsStartTime); - } - - /** - * Returns the resolution with which the timer was instantiated. - * - * @return the resolution of the timer - */ - public Resolution getResolution() { - if (this.resolution == MILLIS_IN_NANO) - return Resolution.MILLISECONDS; - else - return Resolution.SECONDS; - } - - private String resolutionStr() { - if (resolution == SECOND_IN_NANO) { - return "seconds"; - } else if (resolution == MILLIS_IN_NANO) { - return "milliseconds"; - } else { - return "unknown units"; - } - } - - //------------------------------------------------------------------------------------------------ - // Utility - //------------------------------------------------------------------------------------------------ - - /** - * Log a message stating how long the timer has been running - */ - public void log(String label) { - Log.info("ElapsedTime", String.format("TIMER: %20s - %1.3f %s", label, time(), resolutionStr())); - } - - /** - * Returns a string indicating the current elapsed time of the timer. - */ - @Override - public String toString() { - return String.format("%1.4f %s", time(), resolutionStr()); - } - - /** - * An indicator of the resolution of a timer. - * - * @see ElapsedTime#ElapsedTime(Resolution) - */ - public enum Resolution { - SECONDS, - MILLISECONDS - } +/* Copyright (c) 2014, 2015 Qualcomm Technologies Inc +All rights reserved. +Redistribution and use in source and binary forms, with or without modification, +are permitted (subject to the limitations in the disclaimer below) provided that +the following conditions are met: +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. +Neither the name of Qualcomm Technologies Inc nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS +LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +package com.qualcomm.robotcore.util; + +import com.github.serivesmejia.eocvsim.util.Log; + +import java.util.concurrent.TimeUnit; + +/** + * The {@link ElapsedTime} class provides a simple handy timer to measure elapsed time intervals. + * The timer does not provide events or callbacks, as some other timers do. Rather, at an application- + * determined juncture, one can {@link #reset()} the timer. Thereafter, one can query the interval + * of wall-clock time that has subsequently elapsed by calling the {@link #time()}, {@link #seconds()}, + * or {@link #milliseconds()} methods. The timer has nanosecond internal accuracy. The precision + * reported by the {@link #time()} method is either seconds or milliseconds, depending on how the + * timer is initially constructed. + *

+ * This class is thread-safe. + */ +@SuppressWarnings("WeakerAccess") +public class ElapsedTime { + + //------------------------------------------------------------------------------------------------ + // Types and constants + //------------------------------------------------------------------------------------------------ + + /** + * the number of nanoseconds in a second + */ + public static final long SECOND_IN_NANO = 1000000000; + /** + * the number of nanoseconds in a millisecond + */ + public static final long MILLIS_IN_NANO = 1000000; + protected final double resolution; + + //------------------------------------------------------------------------------------------------ + // State + //------------------------------------------------------------------------------------------------ + protected volatile long nsStartTime; + /** + * Creates a timer with resolution {@link com.qualcomm.robotcore.util.ElapsedTime.Resolution#SECONDS Resolution.Seconds} + * that is initialized with the now-current time. + * + * @see #ElapsedTime(long) + * @see #ElapsedTime(Resolution) + */ + public ElapsedTime() { + reset(); + this.resolution = SECOND_IN_NANO; + } + + //------------------------------------------------------------------------------------------------ + // Construction + //------------------------------------------------------------------------------------------------ + + /** + * Creates a timer with resolution {@link com.qualcomm.robotcore.util.ElapsedTime.Resolution#SECONDS Resolution.Seconds}. + * The timer is initialized with the provided start time. Zero is often a useful value to provide + * here: in common usage such timers will often be processed by application logic virtually immediately. + * + * @param startTime the initial value of the timer + * @see #ElapsedTime() + */ + public ElapsedTime(long startTime) { + this.nsStartTime = startTime; + this.resolution = SECOND_IN_NANO; + } + + /** + * Creates a timer with a resolution of seconds or milliseconds. The resolution + * affects the units in which the {@link #time()} method reports. The timer is initialized + * with the current time. + * + * @param resolution the resolution of the new timer + * @see #ElapsedTime() + */ + public ElapsedTime(Resolution resolution) { + reset(); + switch (resolution) { + case SECONDS: + default: + this.resolution = SECOND_IN_NANO; + break; + case MILLISECONDS: + this.resolution = MILLIS_IN_NANO; + break; + } + } + + protected long nsNow() { + return System.nanoTime(); + } + + //------------------------------------------------------------------------------------------------ + // Operations + //------------------------------------------------------------------------------------------------ + + /** + * Returns the current time on the clock used by the timer + * + * @param unit the time unit in which the current time should be returned + * @return the current time on the clock used by the timer + */ + public long now(TimeUnit unit) { + return unit.convert(nsNow(), TimeUnit.NANOSECONDS); + } + + /** + * Resets the internal state of the timer to reflect the current time. Instantaneously following + * this reset, {@link #time()} will report as zero. + * + * @see #time() + */ + public void reset() { + nsStartTime = nsNow(); + } + + /** + * Returns, in resolution-dependent units, the time at which this timer was last reset. + * + * @return the reset time of the timer + */ + public double startTime() { + return nsStartTime / resolution; + } + + /** + * Returns the time at which the timer was last reset, in units of nanoseconds + * + * @return the time at which the timer was last reset, in units of nanoseconds + */ + public long startTimeNanoseconds() { + return this.nsStartTime; + } + + /** + * Returns the duration that has elapsed since the last reset of this timer. + * Units used are either seconds or milliseconds, depending on the resolution with + * which the timer was instantiated. + * + * @return time duration since last timer reset + * @see #ElapsedTime() + * @see #ElapsedTime(Resolution) + * @see #seconds() + * @see #milliseconds() + */ + public double time() { + return (nsNow() - nsStartTime) / resolution; + } + + /** + * Returns the duration that has elapsed since the last reset of this timer + * as an integer in the units requested. + * + * @param unit the units in which to return the answer + * @return time duration since last timer reset + */ + public long time(TimeUnit unit) { + return unit.convert(nanoseconds(), TimeUnit.NANOSECONDS); + } + + /** + * Returns the duration that has elapsed since the last reset of this timer in seconds + * + * @return time duration since last timer reset + * @see #time() + */ + public double seconds() { + return nanoseconds() / ((double) (SECOND_IN_NANO)); + } + + /** + * Returns the duration that has elapsed since the last reset of this timer in milliseconds + * + * @return time duration since last timer reset + * @see #time() + */ + public double milliseconds() { + return seconds() * 1000; + } + + /** + * Returns the duration that has elapsed since the last reset of this timer in nanoseconds + * + * @return time duration since last timer reset + * @see #time() + */ + public long nanoseconds() { + return (nsNow() - nsStartTime); + } + + /** + * Returns the resolution with which the timer was instantiated. + * + * @return the resolution of the timer + */ + public Resolution getResolution() { + if (this.resolution == MILLIS_IN_NANO) + return Resolution.MILLISECONDS; + else + return Resolution.SECONDS; + } + + private String resolutionStr() { + if (resolution == SECOND_IN_NANO) { + return "seconds"; + } else if (resolution == MILLIS_IN_NANO) { + return "milliseconds"; + } else { + return "unknown units"; + } + } + + //------------------------------------------------------------------------------------------------ + // Utility + //------------------------------------------------------------------------------------------------ + + /** + * Log a message stating how long the timer has been running + */ + public void log(String label) { + Log.info("ElapsedTime", String.format("TIMER: %20s - %1.3f %s", label, time(), resolutionStr())); + } + + /** + * Returns a string indicating the current elapsed time of the timer. + */ + @Override + public String toString() { + return String.format("%1.4f %s", time(), resolutionStr()); + } + + /** + * An indicator of the resolution of a timer. + * + * @see ElapsedTime#ElapsedTime(Resolution) + */ + public enum Resolution { + SECONDS, + MILLISECONDS + } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/util/MovingStatistics.java b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/util/MovingStatistics.java index dc730bbd..5b2ec6f6 100644 --- a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/util/MovingStatistics.java +++ b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/util/MovingStatistics.java @@ -1,125 +1,125 @@ -/* -Copyright (c) 2016 Robert Atkinson -All rights reserved. -Redistribution and use in source and binary forms, with or without modification, -are permitted (subject to the limitations in the disclaimer below) provided that -the following conditions are met: -Redistributions of source code must retain the above copyright notice, this list -of conditions and the following disclaimer. -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. -Neither the name of Robert Atkinson nor the names of his contributors may be used to -endorse or promote products derived from this software without specific prior -written permission. -NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS -LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR -TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -package com.qualcomm.robotcore.util; - -import java.util.LinkedList; -import java.util.Queue; - -/** - * MovingStatistics keeps statistics on the most recent samples in a data set, automatically - * removing old samples as the size of the data exceeds a fixed capacity. This class is *not* - * thread-safe. - */ -public class MovingStatistics -{ - //---------------------------------------------------------------------------------------------- - // State - //---------------------------------------------------------------------------------------------- - - final Statistics statistics; - final int capacity; - final Queue samples; - - //---------------------------------------------------------------------------------------------- - // Construction - //---------------------------------------------------------------------------------------------- - - public MovingStatistics(int capacity) - { - if (capacity <= 0) throw new IllegalArgumentException("MovingStatistics capacity must be positive"); - this.statistics = new Statistics(); - this.capacity = capacity; - this.samples = new LinkedList(); - } - - //---------------------------------------------------------------------------------------------- - // Accessing - //---------------------------------------------------------------------------------------------- - - /** - * Returns the current number of samples - * @return the number of samples - */ - public int getCount() - { - return this.statistics.getCount(); - } - - /** - * Returns the mean of the current set of samples - * @return the mean of the samples - */ - public double getMean() - { - return this.statistics.getMean(); - } - - /** - * Returns the sample variance of the current set of samples - * @return the variance of the samples - */ - public double getVariance() - { - return this.statistics.getVariance(); - } - - /** - * Returns the sample standard deviation of the current set of samples - * @return the standard deviation of the samples - */ - public double getStandardDeviation() - { - return this.statistics.getStandardDeviation(); - } - - //---------------------------------------------------------------------------------------------- - // Modifying - //---------------------------------------------------------------------------------------------- - - /** - * Resets the statistics to an empty state - */ - public void clear() - { - this.statistics.clear(); - this.samples.clear(); - } - - /** - * Adds a new sample to the statistics, possibly also removing the oldest. - * @param x the sample to add - */ - public void add(double x) - { - this.statistics.add(x); - this.samples.add(x); - if (this.samples.size() > capacity) - { - this.statistics.remove(this.samples.remove()); - } - } +/* +Copyright (c) 2016 Robert Atkinson +All rights reserved. +Redistribution and use in source and binary forms, with or without modification, +are permitted (subject to the limitations in the disclaimer below) provided that +the following conditions are met: +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. +Neither the name of Robert Atkinson nor the names of his contributors may be used to +endorse or promote products derived from this software without specific prior +written permission. +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS +LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +package com.qualcomm.robotcore.util; + +import java.util.LinkedList; +import java.util.Queue; + +/** + * MovingStatistics keeps statistics on the most recent samples in a data set, automatically + * removing old samples as the size of the data exceeds a fixed capacity. This class is *not* + * thread-safe. + */ +public class MovingStatistics +{ + //---------------------------------------------------------------------------------------------- + // State + //---------------------------------------------------------------------------------------------- + + final Statistics statistics; + final int capacity; + final Queue samples; + + //---------------------------------------------------------------------------------------------- + // Construction + //---------------------------------------------------------------------------------------------- + + public MovingStatistics(int capacity) + { + if (capacity <= 0) throw new IllegalArgumentException("MovingStatistics capacity must be positive"); + this.statistics = new Statistics(); + this.capacity = capacity; + this.samples = new LinkedList(); + } + + //---------------------------------------------------------------------------------------------- + // Accessing + //---------------------------------------------------------------------------------------------- + + /** + * Returns the current number of samples + * @return the number of samples + */ + public int getCount() + { + return this.statistics.getCount(); + } + + /** + * Returns the mean of the current set of samples + * @return the mean of the samples + */ + public double getMean() + { + return this.statistics.getMean(); + } + + /** + * Returns the sample variance of the current set of samples + * @return the variance of the samples + */ + public double getVariance() + { + return this.statistics.getVariance(); + } + + /** + * Returns the sample standard deviation of the current set of samples + * @return the standard deviation of the samples + */ + public double getStandardDeviation() + { + return this.statistics.getStandardDeviation(); + } + + //---------------------------------------------------------------------------------------------- + // Modifying + //---------------------------------------------------------------------------------------------- + + /** + * Resets the statistics to an empty state + */ + public void clear() + { + this.statistics.clear(); + this.samples.clear(); + } + + /** + * Adds a new sample to the statistics, possibly also removing the oldest. + * @param x the sample to add + */ + public void add(double x) + { + this.statistics.add(x); + this.samples.add(x); + if (this.samples.size() > capacity) + { + this.statistics.remove(this.samples.remove()); + } + } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/util/Range.java b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/util/Range.java index 378251a1..16f5eb28 100644 --- a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/util/Range.java +++ b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/util/Range.java @@ -1,158 +1,158 @@ - -/* - * Copyright (c) 2014, 2015 Qualcomm Technologies Inc - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are permitted - * (subject to the limitations in the disclaimer below) provided that the following conditions are - * met: - * - * Redistributions of source code must retain the above copyright notice, this list of conditions - * and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, this list of conditions - * and the following disclaimer in the documentation and/or other materials provided with the - * distribution. - * - * Neither the name of Qualcomm Technologies Inc nor the names of its contributors may be used to - * endorse or promote products derived from this software without specific prior written permission. - * - * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE. THIS - * SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF - * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package com.qualcomm.robotcore.util; - -/** - * Utility class for performing range operations - */ -public class Range { - - /* - * This class contains only static utility methods - */ - private Range() {} - - //------------------------------------------------------------------------------------------------ - // Scaling - //------------------------------------------------------------------------------------------------ - - /** - * Scale a number in the range of x1 to x2, to the range of y1 to y2 - * @param n number to scale - * @param x1 lower bound range of n - * @param x2 upper bound range of n - * @param y1 lower bound of scale - * @param y2 upper bound of scale - * @return a double scaled to a value between y1 and y2, inclusive - */ - public static double scale(double n, double x1, double x2, double y1, double y2) { - double a = (y1-y2)/(x1-x2); - double b = y1 - x1*(y1-y2)/(x1-x2); - return a*n+b; - } - - //------------------------------------------------------------------------------------------------ - // Clipping - //------------------------------------------------------------------------------------------------ - - /** - * clip 'number' if 'number' is less than 'min' or greater than 'max' - * @param number number to test - * @param min minimum value allowed - * @param max maximum value allowed - */ - public static double clip(double number, double min, double max) { - if (number < min) return min; - if (number > max) return max; - return number; - } - - /** - * clip 'number' if 'number' is less than 'min' or greater than 'max' - * @param number number to test - * @param min minimum value allowed - * @param max maximum value allowed - */ - public static float clip(float number, float min, float max) { - if (number < min) return min; - if (number > max) return max; - return number; - } - - /** - * clip 'number' if 'number' is less than 'min' or greater than 'max' - * @param number number to test - * @param min minimum value allowed - * @param max maximum value allowed - */ - public static int clip(int number, int min, int max) { - if (number < min) return min; - if (number > max) return max; - return number; - } - - /** - * clip 'number' if 'number' is less than 'min' or greater than 'max' - * @param number number to test - * @param min minimum value allowed - * @param max maximum value allowed - */ - public static short clip(short number, short min, short max) { - if (number < min) return min; - if (number > max) return max; - return number; - } - - /** - * clip 'number' if 'number' is less than 'min' or greater than 'max' - * @param number number to test - * @param min minimum value allowed - * @param max maximum value allowed - */ - public static byte clip(byte number, byte min, byte max) { - if (number < min) return min; - if (number > max) return max; - return number; - } - - //------------------------------------------------------------------------------------------------ - // Validation - //------------------------------------------------------------------------------------------------ - - /** - * Throw an IllegalArgumentException if 'number' is less than 'min' or greater than 'max' - * @param number number to test - * @param min minimum value allowed - * @param max maximum value allowed - * @throws IllegalArgumentException if number is outside of range - */ - public static void throwIfRangeIsInvalid(double number, double min, double max) throws IllegalArgumentException { - if (number < min || number > max) { - throw new IllegalArgumentException( - String.format("number %f is invalid; valid ranges are %f..%f", number, min, max)); - } - } - - /** - * Throw an IllegalArgumentException if 'number' is less than 'min' or greater than 'max' - * @param number number to test - * @param min minimum value allowed - * @param max maximum value allowed - * @throws IllegalArgumentException if number is outside of range - */ - public static void throwIfRangeIsInvalid(int number, int min, int max) throws IllegalArgumentException { - if (number < min || number > max) { - throw new IllegalArgumentException( - String.format("number %d is invalid; valid ranges are %d..%d", number, min, max)); - } - } + +/* + * Copyright (c) 2014, 2015 Qualcomm Technologies Inc + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * (subject to the limitations in the disclaimer below) provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * Neither the name of Qualcomm Technologies Inc nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE. THIS + * SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.qualcomm.robotcore.util; + +/** + * Utility class for performing range operations + */ +public class Range { + + /* + * This class contains only static utility methods + */ + private Range() {} + + //------------------------------------------------------------------------------------------------ + // Scaling + //------------------------------------------------------------------------------------------------ + + /** + * Scale a number in the range of x1 to x2, to the range of y1 to y2 + * @param n number to scale + * @param x1 lower bound range of n + * @param x2 upper bound range of n + * @param y1 lower bound of scale + * @param y2 upper bound of scale + * @return a double scaled to a value between y1 and y2, inclusive + */ + public static double scale(double n, double x1, double x2, double y1, double y2) { + double a = (y1-y2)/(x1-x2); + double b = y1 - x1*(y1-y2)/(x1-x2); + return a*n+b; + } + + //------------------------------------------------------------------------------------------------ + // Clipping + //------------------------------------------------------------------------------------------------ + + /** + * clip 'number' if 'number' is less than 'min' or greater than 'max' + * @param number number to test + * @param min minimum value allowed + * @param max maximum value allowed + */ + public static double clip(double number, double min, double max) { + if (number < min) return min; + if (number > max) return max; + return number; + } + + /** + * clip 'number' if 'number' is less than 'min' or greater than 'max' + * @param number number to test + * @param min minimum value allowed + * @param max maximum value allowed + */ + public static float clip(float number, float min, float max) { + if (number < min) return min; + if (number > max) return max; + return number; + } + + /** + * clip 'number' if 'number' is less than 'min' or greater than 'max' + * @param number number to test + * @param min minimum value allowed + * @param max maximum value allowed + */ + public static int clip(int number, int min, int max) { + if (number < min) return min; + if (number > max) return max; + return number; + } + + /** + * clip 'number' if 'number' is less than 'min' or greater than 'max' + * @param number number to test + * @param min minimum value allowed + * @param max maximum value allowed + */ + public static short clip(short number, short min, short max) { + if (number < min) return min; + if (number > max) return max; + return number; + } + + /** + * clip 'number' if 'number' is less than 'min' or greater than 'max' + * @param number number to test + * @param min minimum value allowed + * @param max maximum value allowed + */ + public static byte clip(byte number, byte min, byte max) { + if (number < min) return min; + if (number > max) return max; + return number; + } + + //------------------------------------------------------------------------------------------------ + // Validation + //------------------------------------------------------------------------------------------------ + + /** + * Throw an IllegalArgumentException if 'number' is less than 'min' or greater than 'max' + * @param number number to test + * @param min minimum value allowed + * @param max maximum value allowed + * @throws IllegalArgumentException if number is outside of range + */ + public static void throwIfRangeIsInvalid(double number, double min, double max) throws IllegalArgumentException { + if (number < min || number > max) { + throw new IllegalArgumentException( + String.format("number %f is invalid; valid ranges are %f..%f", number, min, max)); + } + } + + /** + * Throw an IllegalArgumentException if 'number' is less than 'min' or greater than 'max' + * @param number number to test + * @param min minimum value allowed + * @param max maximum value allowed + * @throws IllegalArgumentException if number is outside of range + */ + public static void throwIfRangeIsInvalid(int number, int min, int max) throws IllegalArgumentException { + if (number < min || number > max) { + throw new IllegalArgumentException( + String.format("number %d is invalid; valid ranges are %d..%d", number, min, max)); + } + } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/util/Statistics.java b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/util/Statistics.java index 0dc0e20c..520b64f6 100644 --- a/EOCV-Sim/src/main/java/com/qualcomm/robotcore/util/Statistics.java +++ b/EOCV-Sim/src/main/java/com/qualcomm/robotcore/util/Statistics.java @@ -1,140 +1,140 @@ -/* -Copyright (c) 2016 Robert Atkinson -All rights reserved. -Redistribution and use in source and binary forms, with or without modification, -are permitted (subject to the limitations in the disclaimer below) provided that -the following conditions are met: -Redistributions of source code must retain the above copyright notice, this list -of conditions and the following disclaimer. -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. -Neither the name of Robert Atkinson nor the names of his contributors may be used to -endorse or promote products derived from this software without specific prior -written permission. -NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS -LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR -TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -package com.qualcomm.robotcore.util; - -/** - * This handy utility class supports the ongoing calculation of mean and variance of a - * series of numbers. This class is *not* thread-safe. - * - * @see Wikipedia - */ -public class Statistics -{ - //---------------------------------------------------------------------------------------------- - // State - //---------------------------------------------------------------------------------------------- - - int n; - double mean; - double m2; - - //---------------------------------------------------------------------------------------------- - // Construction - //---------------------------------------------------------------------------------------------- - - public Statistics() - { - this.clear(); - } - - //---------------------------------------------------------------------------------------------- - // Accessing - //---------------------------------------------------------------------------------------------- - - /** - * Returns the current number of samples - * @return the number of samples - */ - public int getCount() - { - return n; - } - - /** - * Returns the mean of the current set of samples - * @return the mean of the samples - */ - public double getMean() - { - return mean; - } - - /** - * Returns the sample variance of the current set of samples - * @return the variance of the samples - */ - public double getVariance() - { - return m2 / (n - 1); - } - - /** - * Returns the sample standard deviation of the current set of samples - * @return the standard deviation of the samples - */ - public double getStandardDeviation() - { - return Math.sqrt(this.getVariance()); - } - - //---------------------------------------------------------------------------------------------- - // Modifying - //---------------------------------------------------------------------------------------------- - - /** - * Resets the statistics to an empty state - */ - public void clear() - { - n = 0; - mean = 0; - m2 = 0; - } - - /** - * Adds a new sample to the statistics - * @param x the sample to add - */ - public void add(double x) - { - n = n + 1; - double delta = x - mean; - mean = mean + delta / n; - m2 = m2 + delta*(x - mean); - } - - /** - * Removes a sample from the statistics - * @param x the sample to remove - */ - public void remove(double x) - { - int nPrev = n-1; - if (nPrev==0) - { - clear(); - } - else - { - double delta = x - mean; - double deltaPrev = n * delta / nPrev; - m2 = m2 - deltaPrev * delta; - mean = (mean * n - x) / nPrev; - n = nPrev; - } - } +/* +Copyright (c) 2016 Robert Atkinson +All rights reserved. +Redistribution and use in source and binary forms, with or without modification, +are permitted (subject to the limitations in the disclaimer below) provided that +the following conditions are met: +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. +Neither the name of Robert Atkinson nor the names of his contributors may be used to +endorse or promote products derived from this software without specific prior +written permission. +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS +LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +package com.qualcomm.robotcore.util; + +/** + * This handy utility class supports the ongoing calculation of mean and variance of a + * series of numbers. This class is *not* thread-safe. + * + * @see Wikipedia + */ +public class Statistics +{ + //---------------------------------------------------------------------------------------------- + // State + //---------------------------------------------------------------------------------------------- + + int n; + double mean; + double m2; + + //---------------------------------------------------------------------------------------------- + // Construction + //---------------------------------------------------------------------------------------------- + + public Statistics() + { + this.clear(); + } + + //---------------------------------------------------------------------------------------------- + // Accessing + //---------------------------------------------------------------------------------------------- + + /** + * Returns the current number of samples + * @return the number of samples + */ + public int getCount() + { + return n; + } + + /** + * Returns the mean of the current set of samples + * @return the mean of the samples + */ + public double getMean() + { + return mean; + } + + /** + * Returns the sample variance of the current set of samples + * @return the variance of the samples + */ + public double getVariance() + { + return m2 / (n - 1); + } + + /** + * Returns the sample standard deviation of the current set of samples + * @return the standard deviation of the samples + */ + public double getStandardDeviation() + { + return Math.sqrt(this.getVariance()); + } + + //---------------------------------------------------------------------------------------------- + // Modifying + //---------------------------------------------------------------------------------------------- + + /** + * Resets the statistics to an empty state + */ + public void clear() + { + n = 0; + mean = 0; + m2 = 0; + } + + /** + * Adds a new sample to the statistics + * @param x the sample to add + */ + public void add(double x) + { + n = n + 1; + double delta = x - mean; + mean = mean + delta / n; + m2 = m2 + delta*(x - mean); + } + + /** + * Removes a sample from the statistics + * @param x the sample to remove + */ + public void remove(double x) + { + int nPrev = n-1; + if (nPrev==0) + { + clear(); + } + else + { + double delta = x - mean; + double deltaPrev = n * delta / nPrev; + m2 = m2 - deltaPrev * delta; + mean = (mean * n - x) / nPrev; + n = nPrev; + } + } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/external/Func.java b/EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/external/Func.java index 78847cef..3737d2fe 100644 --- a/EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/external/Func.java +++ b/EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/external/Func.java @@ -1,7 +1,7 @@ -package org.firstinspires.ftc.robotcore.external; - -public interface Func { - - T value(); - -} +package org.firstinspires.ftc.robotcore.external; + +public interface Func { + + T value(); + +} diff --git a/EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/external/Telemetry.java b/EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/external/Telemetry.java index 0d2c2559..2efd7d6a 100644 --- a/EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/external/Telemetry.java +++ b/EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/external/Telemetry.java @@ -1,266 +1,266 @@ -package org.firstinspires.ftc.robotcore.external; - -import java.util.ArrayList; - -public class Telemetry { - - private final ArrayList telem = new ArrayList<>(); - private ArrayList lastTelem = new ArrayList<>(); - - public Item infoItem = new Item( "", ""); - public Item errItem = new Item("", ""); - - private String captionValueSeparator = " : "; - - private volatile String lastTelemUpdate = ""; - private volatile String beforeTelemUpdate = "mai"; - - private boolean autoClear = true; - - public synchronized Item addData(String caption, String value) { - - Item item = new Item(caption, value); - item.valueSeparator = captionValueSeparator; - - telem.add(item); - - return item; - - } - - public synchronized Item addData(String caption, Func valueProducer) { - Item item = new Item(caption, valueProducer); - item.valueSeparator = captionValueSeparator; - - telem.add(item); - - return item; - } - - public synchronized Item addData(String caption, Object value) { - Item item = new Item(caption, ""); - item.valueSeparator = captionValueSeparator; - - item.setValue(value); - - telem.add(item); - - return item; - } - - public synchronized Item addData(String caption, String value, Object... args) { - Item item = new Item(caption, ""); - item.valueSeparator = captionValueSeparator; - - item.setValue(value, args); - - telem.add(item); - - return item; - } - - public synchronized Item addData(String caption, Func valueProducer, Object... args) { - - Item item = new Item(caption, ""); - item.valueSeparator = captionValueSeparator; - - item.setValue(valueProducer, args); - - telem.add(item); - - return item; - - } - - public synchronized Line addLine() { - return addLine(""); - } - - public synchronized Line addLine(String caption) { - Line line = new Line(caption); - telem.add(line); - return line; - } - - @SuppressWarnings("unchecked") - public synchronized void update() { - lastTelemUpdate = ""; - lastTelem = (ArrayList) telem.clone(); - - evalLastTelem(); - - if(autoClear) clear(); - } - - private synchronized void evalLastTelem() { - StringBuilder inTelemUpdate = new StringBuilder(); - - if (infoItem != null && !infoItem.caption.trim().equals("")) { - inTelemUpdate.append(infoItem.toString()).append("\n"); - } - - if(lastTelem != null) { - int i = 0; - for (ItemOrLine iol : lastTelem) { - if (iol instanceof Item) { - Item item = (Item) iol; - item.valueSeparator = captionValueSeparator; - inTelemUpdate.append(item.toString()); //to avoid volatile issues we write into a stringbuilder - } else if (iol instanceof Line) { - Line line = (Line) iol; - inTelemUpdate.append(line.toString()); //to avoid volatile issues we write into a stringbuilder - } - - if (i < lastTelem.size() - 1) - inTelemUpdate.append("\n"); //append new line if this is not the lastest item - - i++; - } - } - - if(errItem != null && !errItem.caption.trim().equals("")) { - inTelemUpdate.append("\n").append(errItem.toString()); - } - - inTelemUpdate.append("\n"); - - lastTelemUpdate = inTelemUpdate.toString().trim(); //and then we write to the volatile, public one - } - - public synchronized boolean removeItem(Item item) { - if (telem.contains(item)) { - telem.remove(item); - return true; - } - - return false; - } - - public synchronized void clear() { - for (ItemOrLine i : telem.toArray(new ItemOrLine[0])) { - if (i instanceof Item) { - if (!((Item) i).isRetained) telem.remove(i); - } else { - telem.remove(i); - } - } - } - - public synchronized boolean hasChanged() { - boolean hasChanged = !lastTelemUpdate.equals(beforeTelemUpdate); - beforeTelemUpdate = lastTelemUpdate; - - return hasChanged; - } - - public synchronized String getCaptionValueSeparator() { - return captionValueSeparator; - } - - public synchronized void setCaptionValueSeparator(String captionValueSeparator) { - this.captionValueSeparator = captionValueSeparator; - } - - public synchronized void setAutoClear(boolean autoClear) { - this.autoClear = autoClear; - } - - @Override - public String toString() { - evalLastTelem(); - return lastTelemUpdate; - } - - private interface ItemOrLine { - String getCaption(); - - void setCaption(String caption); - } - - public static class Item implements ItemOrLine { - - protected String caption = ""; - - protected Func valueProducer = null; - - protected String valueSeparator = " : "; - - protected boolean isRetained = false; - - public Item(String caption, String value) { - setCaption(caption); - setValue(value); - } - - public Item(String caption, Func valueProducer) { - this.caption = caption; - this.valueProducer = valueProducer; - } - - public synchronized void setValue(String value) { - setValue((Func) () -> value); - } - - public synchronized void setValue(Func func) { - this.valueProducer = func; - } - - public synchronized void setValue(Object value) { - setValue(value.toString()); - } - - public synchronized void setValue(String value, Object... args) { - setValue(String.format(value, args)); - } - - public synchronized void setValue(Func func, Object... args) { - setValue((Func) () -> String.format(func.value().toString(), args)); - } - - public synchronized String getCaption() { - return caption; - } - - public synchronized void setCaption(String caption) { - this.caption = caption; - } - - public synchronized boolean isRetained() { - return isRetained; - } - - public synchronized void setRetained(boolean retained) { - this.isRetained = retained; - } - - @Override - public String toString() { - return caption + " " + valueSeparator + " " + valueProducer.value().toString(); - } - - } - - public static class Line implements ItemOrLine { - - protected String caption; - - public Line(String caption) { - this.caption = caption; - } - - public synchronized String getCaption() { - return caption; - } - - public synchronized void setCaption(String caption) { - this.caption = caption; - } - - @Override - public synchronized String toString() { - return caption; - } - - } - -} +package org.firstinspires.ftc.robotcore.external; + +import java.util.ArrayList; + +public class Telemetry { + + private final ArrayList telem = new ArrayList<>(); + private ArrayList lastTelem = new ArrayList<>(); + + public Item infoItem = new Item( "", ""); + public Item errItem = new Item("", ""); + + private String captionValueSeparator = " : "; + + private volatile String lastTelemUpdate = ""; + private volatile String beforeTelemUpdate = "mai"; + + private boolean autoClear = true; + + public synchronized Item addData(String caption, String value) { + + Item item = new Item(caption, value); + item.valueSeparator = captionValueSeparator; + + telem.add(item); + + return item; + + } + + public synchronized Item addData(String caption, Func valueProducer) { + Item item = new Item(caption, valueProducer); + item.valueSeparator = captionValueSeparator; + + telem.add(item); + + return item; + } + + public synchronized Item addData(String caption, Object value) { + Item item = new Item(caption, ""); + item.valueSeparator = captionValueSeparator; + + item.setValue(value); + + telem.add(item); + + return item; + } + + public synchronized Item addData(String caption, String value, Object... args) { + Item item = new Item(caption, ""); + item.valueSeparator = captionValueSeparator; + + item.setValue(value, args); + + telem.add(item); + + return item; + } + + public synchronized Item addData(String caption, Func valueProducer, Object... args) { + + Item item = new Item(caption, ""); + item.valueSeparator = captionValueSeparator; + + item.setValue(valueProducer, args); + + telem.add(item); + + return item; + + } + + public synchronized Line addLine() { + return addLine(""); + } + + public synchronized Line addLine(String caption) { + Line line = new Line(caption); + telem.add(line); + return line; + } + + @SuppressWarnings("unchecked") + public synchronized void update() { + lastTelemUpdate = ""; + lastTelem = (ArrayList) telem.clone(); + + evalLastTelem(); + + if(autoClear) clear(); + } + + private synchronized void evalLastTelem() { + StringBuilder inTelemUpdate = new StringBuilder(); + + if (infoItem != null && !infoItem.caption.trim().equals("")) { + inTelemUpdate.append(infoItem.toString()).append("\n"); + } + + if(lastTelem != null) { + int i = 0; + for (ItemOrLine iol : lastTelem) { + if (iol instanceof Item) { + Item item = (Item) iol; + item.valueSeparator = captionValueSeparator; + inTelemUpdate.append(item.toString()); //to avoid volatile issues we write into a stringbuilder + } else if (iol instanceof Line) { + Line line = (Line) iol; + inTelemUpdate.append(line.toString()); //to avoid volatile issues we write into a stringbuilder + } + + if (i < lastTelem.size() - 1) + inTelemUpdate.append("\n"); //append new line if this is not the lastest item + + i++; + } + } + + if(errItem != null && !errItem.caption.trim().equals("")) { + inTelemUpdate.append("\n").append(errItem.toString()); + } + + inTelemUpdate.append("\n"); + + lastTelemUpdate = inTelemUpdate.toString().trim(); //and then we write to the volatile, public one + } + + public synchronized boolean removeItem(Item item) { + if (telem.contains(item)) { + telem.remove(item); + return true; + } + + return false; + } + + public synchronized void clear() { + for (ItemOrLine i : telem.toArray(new ItemOrLine[0])) { + if (i instanceof Item) { + if (!((Item) i).isRetained) telem.remove(i); + } else { + telem.remove(i); + } + } + } + + public synchronized boolean hasChanged() { + boolean hasChanged = !lastTelemUpdate.equals(beforeTelemUpdate); + beforeTelemUpdate = lastTelemUpdate; + + return hasChanged; + } + + public synchronized String getCaptionValueSeparator() { + return captionValueSeparator; + } + + public synchronized void setCaptionValueSeparator(String captionValueSeparator) { + this.captionValueSeparator = captionValueSeparator; + } + + public synchronized void setAutoClear(boolean autoClear) { + this.autoClear = autoClear; + } + + @Override + public String toString() { + evalLastTelem(); + return lastTelemUpdate; + } + + private interface ItemOrLine { + String getCaption(); + + void setCaption(String caption); + } + + public static class Item implements ItemOrLine { + + protected String caption = ""; + + protected Func valueProducer = null; + + protected String valueSeparator = " : "; + + protected boolean isRetained = false; + + public Item(String caption, String value) { + setCaption(caption); + setValue(value); + } + + public Item(String caption, Func valueProducer) { + this.caption = caption; + this.valueProducer = valueProducer; + } + + public synchronized void setValue(String value) { + setValue((Func) () -> value); + } + + public synchronized void setValue(Func func) { + this.valueProducer = func; + } + + public synchronized void setValue(Object value) { + setValue(value.toString()); + } + + public synchronized void setValue(String value, Object... args) { + setValue(String.format(value, args)); + } + + public synchronized void setValue(Func func, Object... args) { + setValue((Func) () -> String.format(func.value().toString(), args)); + } + + public synchronized String getCaption() { + return caption; + } + + public synchronized void setCaption(String caption) { + this.caption = caption; + } + + public synchronized boolean isRetained() { + return isRetained; + } + + public synchronized void setRetained(boolean retained) { + this.isRetained = retained; + } + + @Override + public String toString() { + return caption + " " + valueSeparator + " " + valueProducer.value().toString(); + } + + } + + public static class Line implements ItemOrLine { + + protected String caption; + + public Line(String caption) { + this.caption = caption; + } + + public synchronized String getCaption() { + return caption; + } + + public synchronized void setCaption(String caption) { + this.caption = caption; + } + + @Override + public synchronized String toString() { + return caption; + } + + } + +} diff --git a/EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/external/function/Consumer.java b/EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/external/function/Consumer.java index e3bf77a3..640cf070 100644 --- a/EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/external/function/Consumer.java +++ b/EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/external/function/Consumer.java @@ -1,39 +1,39 @@ -/* -Copyright (c) 2016 Robert Atkinson -All rights reserved. -Redistribution and use in source and binary forms, with or without modification, -are permitted (subject to the limitations in the disclaimer below) provided that -the following conditions are met: -Redistributions of source code must retain the above copyright notice, this list -of conditions and the following disclaimer. -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. -Neither the name of Robert Atkinson nor the names of his contributors may be used to -endorse or promote products derived from this software without specific prior -written permission. -NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS -LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR -TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -package org.firstinspires.ftc.robotcore.external.function; - -/** - * Instances of {@link Consumer} are functions that act on an instance of a indicated type - */ -public interface Consumer { - /** - * Performs this operation on the given argument. - * - * @param value the input argument - */ - void accept(T value); +/* +Copyright (c) 2016 Robert Atkinson +All rights reserved. +Redistribution and use in source and binary forms, with or without modification, +are permitted (subject to the limitations in the disclaimer below) provided that +the following conditions are met: +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. +Neither the name of Robert Atkinson nor the names of his contributors may be used to +endorse or promote products derived from this software without specific prior +written permission. +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS +LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +package org.firstinspires.ftc.robotcore.external.function; + +/** + * Instances of {@link Consumer} are functions that act on an instance of a indicated type + */ +public interface Consumer { + /** + * Performs this operation on the given argument. + * + * @param value the input argument + */ + void accept(T value); } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/internal/collections/EvictingBlockingQueue.java b/EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/internal/collections/EvictingBlockingQueue.java index f68bc53a..e64f66a6 100644 --- a/EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/internal/collections/EvictingBlockingQueue.java +++ b/EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/internal/collections/EvictingBlockingQueue.java @@ -1,204 +1,204 @@ -/* -Copyright (c) 2016 Robert Atkinson -All rights reserved. -Redistribution and use in source and binary forms, with or without modification, -are permitted (subject to the limitations in the disclaimer below) provided that -the following conditions are met: -Redistributions of source code must retain the above copyright notice, this list -of conditions and the following disclaimer. -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. -Neither the name of Robert Atkinson nor the names of his contributors may be used to -endorse or promote products derived from this software without specific prior -written permission. -NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS -LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR -TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -package org.firstinspires.ftc.robotcore.internal.collections; - -import com.qualcomm.robotcore.util.ElapsedTime; -import org.firstinspires.ftc.robotcore.external.function.Consumer; -import org.firstinspires.ftc.robotcore.internal.system.Assert; - -import java.util.AbstractQueue; -import java.util.Collection; -import java.util.Iterator; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.TimeUnit; - -/** - * {@link EvictingBlockingQueue} is a {@link BlockingQueue} that evicts old elements - * rather than failing when new data is added to the queue. - */ -@SuppressWarnings("WeakerAccess") -public class EvictingBlockingQueue extends AbstractQueue implements BlockingQueue { - //---------------------------------------------------------------------------------------------- - // State - // - // The central implementation idea is that we must hold theLock to make any additions, and - // must then with lock held ensure capacity by evicting if necessary before doing any addition. - // Removals also take the lock so we don't evict data unncessarily. - //---------------------------------------------------------------------------------------------- - - protected final Object theLock = new Object(); - protected BlockingQueue targetQueue; - protected Consumer evictAction = null; - - //---------------------------------------------------------------------------------------------- - // Construction - //---------------------------------------------------------------------------------------------- - - /** - * Constructs an EvictingBlockingQueue using the target queue as an implementation. The - * target queue must have a capacity of at least one. - * - * @param targetQueue the underlying implementation queue from which we will auto-evict as needed - */ - public EvictingBlockingQueue(BlockingQueue targetQueue) { - this.targetQueue = targetQueue; - } - - public void setEvictAction(Consumer evictAction) { - synchronized (theLock) { - this.evictAction = evictAction; - } - } - - //---------------------------------------------------------------------------------------------- - // AbstractCollection - //---------------------------------------------------------------------------------------------- - - @Override - public Iterator iterator() { - return targetQueue.iterator(); - } - - @Override - public int size() { - return targetQueue.size(); - } - - //---------------------------------------------------------------------------------------------- - // Core: the hard parts - //---------------------------------------------------------------------------------------------- - - @Override - public boolean offer(E e) { - synchronized (theLock) { - if (targetQueue.remainingCapacity() == 0) { - E evicted = targetQueue.poll(); - Assert.assertNotNull(evicted); - if (evictAction != null) { - evictAction.accept(evicted); - } - } - boolean result = targetQueue.offer(e); - Assert.assertTrue(result); - theLock.notifyAll(); // pending polls/takes are worth trying again - return result; - } - } - - @Override - public E take() throws InterruptedException { - synchronized (theLock) { - for (; ; ) { - // Can we get something? Return if we can. - E result = poll(); - if (result != null) - return result; - - // Punt if we've been asked to - if (Thread.currentThread().isInterrupted()) - throw new InterruptedException(); - - // Wait and then try again - theLock.wait(); - } - } - } - - @Override - public E poll(long timeout, TimeUnit unit) throws InterruptedException { - synchronized (theLock) { - final long deadline = System.nanoTime() + unit.toNanos(timeout); - for (; ; ) { - // Can we get something? Return if we can. - E result = poll(); - if (result != null) - return result; - - // Punt if we've been asked to - if (Thread.currentThread().isInterrupted()) - throw new InterruptedException(); - - // How much longer can we wait? - long remaining = deadline - System.nanoTime(); - if (remaining > 0) { - // Wait up to that much and then try again - long ms = remaining / ElapsedTime.MILLIS_IN_NANO; - long ns = remaining - ms * ElapsedTime.MILLIS_IN_NANO; - theLock.wait(ms, (int) ns); - } else - return null; - } - } - } - - //---------------------------------------------------------------------------------------------- - // Remaining parts - //---------------------------------------------------------------------------------------------- - - @Override - public E poll() { - synchronized (theLock) { - return targetQueue.poll(); - } - } - - @Override - public E peek() { - return targetQueue.peek(); - } - - @Override - public void put(E e) throws InterruptedException { - offer(e); - } - - @Override - public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException { - // We will never block because we're full, so the timeouts are unnecessary - return offer(e); - } - - @Override - public int remainingCapacity() { - // We *always* have capacity - return Math.max(targetQueue.remainingCapacity(), 1); - } - - @Override - public int drainTo(Collection c) { - synchronized (theLock) { - return targetQueue.drainTo(c); - } - } - - @Override - public int drainTo(Collection c, int maxElements) { - synchronized (theLock) { - return targetQueue.drainTo(c, maxElements); - } - } +/* +Copyright (c) 2016 Robert Atkinson +All rights reserved. +Redistribution and use in source and binary forms, with or without modification, +are permitted (subject to the limitations in the disclaimer below) provided that +the following conditions are met: +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. +Neither the name of Robert Atkinson nor the names of his contributors may be used to +endorse or promote products derived from this software without specific prior +written permission. +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS +LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +package org.firstinspires.ftc.robotcore.internal.collections; + +import com.qualcomm.robotcore.util.ElapsedTime; +import org.firstinspires.ftc.robotcore.external.function.Consumer; +import org.firstinspires.ftc.robotcore.internal.system.Assert; + +import java.util.AbstractQueue; +import java.util.Collection; +import java.util.Iterator; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * {@link EvictingBlockingQueue} is a {@link BlockingQueue} that evicts old elements + * rather than failing when new data is added to the queue. + */ +@SuppressWarnings("WeakerAccess") +public class EvictingBlockingQueue extends AbstractQueue implements BlockingQueue { + //---------------------------------------------------------------------------------------------- + // State + // + // The central implementation idea is that we must hold theLock to make any additions, and + // must then with lock held ensure capacity by evicting if necessary before doing any addition. + // Removals also take the lock so we don't evict data unncessarily. + //---------------------------------------------------------------------------------------------- + + protected final Object theLock = new Object(); + protected BlockingQueue targetQueue; + protected Consumer evictAction = null; + + //---------------------------------------------------------------------------------------------- + // Construction + //---------------------------------------------------------------------------------------------- + + /** + * Constructs an EvictingBlockingQueue using the target queue as an implementation. The + * target queue must have a capacity of at least one. + * + * @param targetQueue the underlying implementation queue from which we will auto-evict as needed + */ + public EvictingBlockingQueue(BlockingQueue targetQueue) { + this.targetQueue = targetQueue; + } + + public void setEvictAction(Consumer evictAction) { + synchronized (theLock) { + this.evictAction = evictAction; + } + } + + //---------------------------------------------------------------------------------------------- + // AbstractCollection + //---------------------------------------------------------------------------------------------- + + @Override + public Iterator iterator() { + return targetQueue.iterator(); + } + + @Override + public int size() { + return targetQueue.size(); + } + + //---------------------------------------------------------------------------------------------- + // Core: the hard parts + //---------------------------------------------------------------------------------------------- + + @Override + public boolean offer(E e) { + synchronized (theLock) { + if (targetQueue.remainingCapacity() == 0) { + E evicted = targetQueue.poll(); + Assert.assertNotNull(evicted); + if (evictAction != null) { + evictAction.accept(evicted); + } + } + boolean result = targetQueue.offer(e); + Assert.assertTrue(result); + theLock.notifyAll(); // pending polls/takes are worth trying again + return result; + } + } + + @Override + public E take() throws InterruptedException { + synchronized (theLock) { + for (; ; ) { + // Can we get something? Return if we can. + E result = poll(); + if (result != null) + return result; + + // Punt if we've been asked to + if (Thread.currentThread().isInterrupted()) + throw new InterruptedException(); + + // Wait and then try again + theLock.wait(); + } + } + } + + @Override + public E poll(long timeout, TimeUnit unit) throws InterruptedException { + synchronized (theLock) { + final long deadline = System.nanoTime() + unit.toNanos(timeout); + for (; ; ) { + // Can we get something? Return if we can. + E result = poll(); + if (result != null) + return result; + + // Punt if we've been asked to + if (Thread.currentThread().isInterrupted()) + throw new InterruptedException(); + + // How much longer can we wait? + long remaining = deadline - System.nanoTime(); + if (remaining > 0) { + // Wait up to that much and then try again + long ms = remaining / ElapsedTime.MILLIS_IN_NANO; + long ns = remaining - ms * ElapsedTime.MILLIS_IN_NANO; + theLock.wait(ms, (int) ns); + } else + return null; + } + } + } + + //---------------------------------------------------------------------------------------------- + // Remaining parts + //---------------------------------------------------------------------------------------------- + + @Override + public E poll() { + synchronized (theLock) { + return targetQueue.poll(); + } + } + + @Override + public E peek() { + return targetQueue.peek(); + } + + @Override + public void put(E e) throws InterruptedException { + offer(e); + } + + @Override + public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException { + // We will never block because we're full, so the timeouts are unnecessary + return offer(e); + } + + @Override + public int remainingCapacity() { + // We *always* have capacity + return Math.max(targetQueue.remainingCapacity(), 1); + } + + @Override + public int drainTo(Collection c) { + synchronized (theLock) { + return targetQueue.drainTo(c); + } + } + + @Override + public int drainTo(Collection c, int maxElements) { + synchronized (theLock) { + return targetQueue.drainTo(c, maxElements); + } + } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/internal/system/Assert.java b/EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/internal/system/Assert.java index cb5e676b..391749f3 100644 --- a/EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/internal/system/Assert.java +++ b/EOCV-Sim/src/main/java/org/firstinspires/ftc/robotcore/internal/system/Assert.java @@ -1,116 +1,116 @@ -/* -Copyright (c) 2016 Robert Atkinson -All rights reserved. -Redistribution and use in source and binary forms, with or without modification, -are permitted (subject to the limitations in the disclaimer below) provided that -the following conditions are met: -Redistributions of source code must retain the above copyright notice, this list -of conditions and the following disclaimer. -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. -Neither the name of Robert Atkinson nor the names of his contributors may be used to -endorse or promote products derived from this software without specific prior -written permission. -NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS -LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR -TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -package org.firstinspires.ftc.robotcore.internal.system; - -import com.github.serivesmejia.eocvsim.util.Log; - -/** - * {@link Assert} is a utility class for assertions that generates an exception which - * gets written to the log, but then continues on with the application. The write to the - * log gives us notices that something is amiss that needs addressing, but continuing with - * the app might, for example, allow a robot to continue on in a match rather than aborting - * in the middle, depending on the nature of the failure. - */ -public class Assert { - public static final String TAG = "Assert"; - - public static void assertTrue(boolean value) { - if (!value) { - assertFailed(); - } - } - - public static void assertFalse(boolean value) { - if (value) { - assertFailed(); - } - } - - public static void assertNull(Object value) { - if (value != null) { - assertFailed(); - } - } - - public static void assertNotNull(Object value) { - if (value == null) { - assertFailed(); - } - } - - public static void assertEquals(int expected, int actual) { - if (expected != actual) { - assertFailed(); - } - } - - //---------------------------------------------------------------------------------------------- - - public static void assertTrue(boolean value, String format, Object... args) { - if (!value) { - assertFailed(format, args); - } - } - - public static void assertFalse(boolean value, String format, Object... args) { - if (value) { - assertFailed(format, args); - } - } - - public static void assertNull(Object value, String format, Object... args) { - if (value != null) { - assertFailed(format, args); - } - } - - public static void assertNotNull(Object value, String format, Object... args) { - if (value == null) { - assertFailed(format, args); - } - } - - //---------------------------------------------------------------------------------------------- - - public static void assertFailed() { - try { - throw new RuntimeException("assertion failed"); - } catch (Exception e) { - Log.error(TAG, "assertion failed", e); - } - } - - public static void assertFailed(String format, Object[] args) { - String message = String.format(format, args); - String banner = "assertion failed: " + message; - try { - throw new RuntimeException(banner); - } catch (Exception e) { - Log.error(TAG, banner, e); - } - } +/* +Copyright (c) 2016 Robert Atkinson +All rights reserved. +Redistribution and use in source and binary forms, with or without modification, +are permitted (subject to the limitations in the disclaimer below) provided that +the following conditions are met: +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. +Neither the name of Robert Atkinson nor the names of his contributors may be used to +endorse or promote products derived from this software without specific prior +written permission. +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS +LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +package org.firstinspires.ftc.robotcore.internal.system; + +import com.github.serivesmejia.eocvsim.util.Log; + +/** + * {@link Assert} is a utility class for assertions that generates an exception which + * gets written to the log, but then continues on with the application. The write to the + * log gives us notices that something is amiss that needs addressing, but continuing with + * the app might, for example, allow a robot to continue on in a match rather than aborting + * in the middle, depending on the nature of the failure. + */ +public class Assert { + public static final String TAG = "Assert"; + + public static void assertTrue(boolean value) { + if (!value) { + assertFailed(); + } + } + + public static void assertFalse(boolean value) { + if (value) { + assertFailed(); + } + } + + public static void assertNull(Object value) { + if (value != null) { + assertFailed(); + } + } + + public static void assertNotNull(Object value) { + if (value == null) { + assertFailed(); + } + } + + public static void assertEquals(int expected, int actual) { + if (expected != actual) { + assertFailed(); + } + } + + //---------------------------------------------------------------------------------------------- + + public static void assertTrue(boolean value, String format, Object... args) { + if (!value) { + assertFailed(format, args); + } + } + + public static void assertFalse(boolean value, String format, Object... args) { + if (value) { + assertFailed(format, args); + } + } + + public static void assertNull(Object value, String format, Object... args) { + if (value != null) { + assertFailed(format, args); + } + } + + public static void assertNotNull(Object value, String format, Object... args) { + if (value == null) { + assertFailed(format, args); + } + } + + //---------------------------------------------------------------------------------------------- + + public static void assertFailed() { + try { + throw new RuntimeException("assertion failed"); + } catch (Exception e) { + Log.error(TAG, "assertion failed", e); + } + } + + public static void assertFailed(String format, Object[] args) { + String message = String.format(format, args); + String banner = "assertion failed: " + message; + try { + throw new RuntimeException(banner); + } catch (Exception e) { + Log.error(TAG, banner, e); + } + } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/org/openftc/easyopencv/MatRecycler.java b/EOCV-Sim/src/main/java/org/openftc/easyopencv/MatRecycler.java index 98d4578a..1f11b825 100644 --- a/EOCV-Sim/src/main/java/org/openftc/easyopencv/MatRecycler.java +++ b/EOCV-Sim/src/main/java/org/openftc/easyopencv/MatRecycler.java @@ -1,113 +1,113 @@ -/* - * Copyright (c) 2019 OpenFTC Team - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.openftc.easyopencv; - -import com.github.serivesmejia.eocvsim.util.Log; -import org.opencv.core.Mat; - -import java.util.concurrent.ArrayBlockingQueue; - -/* - * A utility class for managing the re-use of Mats - * so as to re-use already allocated memory instead - * of constantly allocating new Mats and then freeing - * them after use. - */ -public class MatRecycler { - private final RecyclableMat[] mats; - private final ArrayBlockingQueue availableMats; - - public MatRecycler(int num) { - mats = new RecyclableMat[num]; - availableMats = new ArrayBlockingQueue<>(num); - - for (int i = 0; i < mats.length; i++) { - mats[i] = new RecyclableMat(i); - availableMats.add(mats[i]); - } - } - - public synchronized RecyclableMat takeMat() { - if (availableMats.size() == 0) { - throw new RuntimeException("All mats have been checked out!"); - } - - RecyclableMat mat = null; - try { - mat = availableMats.take(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - - mat.checkedOut = true; - return mat; - - } - - public synchronized void returnMat(RecyclableMat mat) { - if (mat != mats[mat.idx]) { - throw new IllegalArgumentException("This mat does not belong to this recycler!"); - } - - if (mat.checkedOut) { - mat.checkedOut = false; - availableMats.add(mat); - } else { - throw new IllegalArgumentException("This mat has already been returned!"); - } - } - - public void releaseAll() { - for (Mat mat : mats) { - mat.release(); - } - } - - public int getSize() { - return mats.length; - } - - public int getAvailableMatsAmount() { return availableMats.size(); } - - public final class RecyclableMat extends Mat { - - private int idx = -1; - private volatile boolean checkedOut = false; - - private RecyclableMat(int idx) { - this.idx = idx; - } - - public void returnMat() { - synchronized(MatRecycler.this) { - try { - MatRecycler.this.returnMat(this); - } catch (IllegalArgumentException ex) { - Log.warn("RecyclableMat", "Tried to return a Mat which was already returned", ex); - } - } - } - - public boolean isCheckedOut() { return checkedOut; } - - } -} +/* + * Copyright (c) 2019 OpenFTC Team + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.openftc.easyopencv; + +import com.github.serivesmejia.eocvsim.util.Log; +import org.opencv.core.Mat; + +import java.util.concurrent.ArrayBlockingQueue; + +/* + * A utility class for managing the re-use of Mats + * so as to re-use already allocated memory instead + * of constantly allocating new Mats and then freeing + * them after use. + */ +public class MatRecycler { + private final RecyclableMat[] mats; + private final ArrayBlockingQueue availableMats; + + public MatRecycler(int num) { + mats = new RecyclableMat[num]; + availableMats = new ArrayBlockingQueue<>(num); + + for (int i = 0; i < mats.length; i++) { + mats[i] = new RecyclableMat(i); + availableMats.add(mats[i]); + } + } + + public synchronized RecyclableMat takeMat() { + if (availableMats.size() == 0) { + throw new RuntimeException("All mats have been checked out!"); + } + + RecyclableMat mat = null; + try { + mat = availableMats.take(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + mat.checkedOut = true; + return mat; + + } + + public synchronized void returnMat(RecyclableMat mat) { + if (mat != mats[mat.idx]) { + throw new IllegalArgumentException("This mat does not belong to this recycler!"); + } + + if (mat.checkedOut) { + mat.checkedOut = false; + availableMats.add(mat); + } else { + throw new IllegalArgumentException("This mat has already been returned!"); + } + } + + public void releaseAll() { + for (Mat mat : mats) { + mat.release(); + } + } + + public int getSize() { + return mats.length; + } + + public int getAvailableMatsAmount() { return availableMats.size(); } + + public final class RecyclableMat extends Mat { + + private int idx = -1; + private volatile boolean checkedOut = false; + + private RecyclableMat(int idx) { + this.idx = idx; + } + + public void returnMat() { + synchronized(MatRecycler.this) { + try { + MatRecycler.this.returnMat(this); + } catch (IllegalArgumentException ex) { + Log.warn("RecyclableMat", "Tried to return a Mat which was already returned", ex); + } + } + } + + public boolean isCheckedOut() { return checkedOut; } + + } +} diff --git a/EOCV-Sim/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java b/EOCV-Sim/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java index 5093888c..c108e94b 100644 --- a/EOCV-Sim/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java +++ b/EOCV-Sim/src/main/java/org/openftc/easyopencv/OpenCvPipeline.java @@ -1,13 +1,13 @@ -package org.openftc.easyopencv; - -import org.opencv.core.Mat; - -public abstract class OpenCvPipeline { - - public abstract Mat processFrame(Mat input); - - public void onViewportTapped() { } - - public void init(Mat mat) { } - +package org.openftc.easyopencv; + +import org.opencv.core.Mat; + +public abstract class OpenCvPipeline { + + public abstract Mat processFrame(Mat input); + + public void onViewportTapped() { } + + public void init(Mat mat) { } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/org/openftc/easyopencv/OpenCvTracker.java b/EOCV-Sim/src/main/java/org/openftc/easyopencv/OpenCvTracker.java index a0042a68..a1422e4f 100644 --- a/EOCV-Sim/src/main/java/org/openftc/easyopencv/OpenCvTracker.java +++ b/EOCV-Sim/src/main/java/org/openftc/easyopencv/OpenCvTracker.java @@ -1,35 +1,35 @@ -/* - * Copyright (c) 2019 OpenFTC Team - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.openftc.easyopencv; - -import org.opencv.core.Mat; - -public abstract class OpenCvTracker { - private final Mat mat = new Mat(); - - public abstract Mat processFrame(Mat input); - - protected final Mat processFrameInternal(Mat input) { - input.copyTo(mat); - return processFrame(mat); - } +/* + * Copyright (c) 2019 OpenFTC Team + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.openftc.easyopencv; + +import org.opencv.core.Mat; + +public abstract class OpenCvTracker { + private final Mat mat = new Mat(); + + public abstract Mat processFrame(Mat input); + + protected final Mat processFrameInternal(Mat input) { + input.copyTo(mat); + return processFrame(mat); + } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/org/openftc/easyopencv/OpenCvTrackerApiPipeline.java b/EOCV-Sim/src/main/java/org/openftc/easyopencv/OpenCvTrackerApiPipeline.java index 406c2a0b..1f4c4504 100644 --- a/EOCV-Sim/src/main/java/org/openftc/easyopencv/OpenCvTrackerApiPipeline.java +++ b/EOCV-Sim/src/main/java/org/openftc/easyopencv/OpenCvTrackerApiPipeline.java @@ -1,71 +1,71 @@ -/* - * Copyright (c) 2019 OpenFTC Team - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.openftc.easyopencv; - -import org.opencv.core.Mat; - -import java.util.ArrayList; - -public class OpenCvTrackerApiPipeline extends OpenCvPipeline { - private final ArrayList trackers = new ArrayList<>(); - private int trackerDisplayIdx = 0; - - public synchronized void addTracker(OpenCvTracker tracker) { - trackers.add(tracker); - } - - public synchronized void removeTracker(OpenCvTracker tracker) { - trackers.remove(tracker); - - if (trackerDisplayIdx >= trackers.size()) { - trackerDisplayIdx--; - - if (trackerDisplayIdx < 0) { - trackerDisplayIdx = 0; - } - } - } - - @Override - public synchronized Mat processFrame(Mat input) { - if (trackers.size() == 0) { - return input; - } - - ArrayList returnMats = new ArrayList<>(trackers.size()); - - for (OpenCvTracker tracker : trackers) { - returnMats.add(tracker.processFrameInternal(input)); - } - - return returnMats.get(trackerDisplayIdx); - } - - @Override - public synchronized void onViewportTapped() { - trackerDisplayIdx++; - - if (trackerDisplayIdx >= trackers.size()) { - trackerDisplayIdx = 0; - } - } +/* + * Copyright (c) 2019 OpenFTC Team + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.openftc.easyopencv; + +import org.opencv.core.Mat; + +import java.util.ArrayList; + +public class OpenCvTrackerApiPipeline extends OpenCvPipeline { + private final ArrayList trackers = new ArrayList<>(); + private int trackerDisplayIdx = 0; + + public synchronized void addTracker(OpenCvTracker tracker) { + trackers.add(tracker); + } + + public synchronized void removeTracker(OpenCvTracker tracker) { + trackers.remove(tracker); + + if (trackerDisplayIdx >= trackers.size()) { + trackerDisplayIdx--; + + if (trackerDisplayIdx < 0) { + trackerDisplayIdx = 0; + } + } + } + + @Override + public synchronized Mat processFrame(Mat input) { + if (trackers.size() == 0) { + return input; + } + + ArrayList returnMats = new ArrayList<>(trackers.size()); + + for (OpenCvTracker tracker : trackers) { + returnMats.add(tracker.processFrameInternal(input)); + } + + return returnMats.get(trackerDisplayIdx); + } + + @Override + public synchronized void onViewportTapped() { + trackerDisplayIdx++; + + if (trackerDisplayIdx >= trackers.size()) { + trackerDisplayIdx = 0; + } + } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/org/openftc/easyopencv/TimestampedOpenCvPipeline.java b/EOCV-Sim/src/main/java/org/openftc/easyopencv/TimestampedOpenCvPipeline.java index 7281d245..e2453bae 100644 --- a/EOCV-Sim/src/main/java/org/openftc/easyopencv/TimestampedOpenCvPipeline.java +++ b/EOCV-Sim/src/main/java/org/openftc/easyopencv/TimestampedOpenCvPipeline.java @@ -1,42 +1,42 @@ -/* - * Copyright (c) 2020 OpenFTC Team - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.openftc.easyopencv; - -import org.opencv.core.Mat; - -public abstract class TimestampedOpenCvPipeline extends OpenCvPipeline -{ - private long timestamp; - - @Override - public final Mat processFrame(Mat input) - { - return processFrame(input, timestamp); - } - - public abstract Mat processFrame(Mat input, long captureTimeNanos); - - protected void setTimestamp(long timestamp) - { - this.timestamp = timestamp; - } +/* + * Copyright (c) 2020 OpenFTC Team + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.openftc.easyopencv; + +import org.opencv.core.Mat; + +public abstract class TimestampedOpenCvPipeline extends OpenCvPipeline +{ + private long timestamp; + + @Override + public final Mat processFrame(Mat input) + { + return processFrame(input, timestamp); + } + + public abstract Mat processFrame(Mat input, long captureTimeNanos); + + protected void setTimestamp(long timestamp) + { + this.timestamp = timestamp; + } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/org/openftc/easyopencv/TimestampedPipelineHandler.kt b/EOCV-Sim/src/main/java/org/openftc/easyopencv/TimestampedPipelineHandler.kt index 3cc94c9a..74248561 100644 --- a/EOCV-Sim/src/main/java/org/openftc/easyopencv/TimestampedPipelineHandler.kt +++ b/EOCV-Sim/src/main/java/org/openftc/easyopencv/TimestampedPipelineHandler.kt @@ -1,36 +1,36 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package org.openftc.easyopencv - -import com.github.serivesmejia.eocvsim.input.InputSource - -class TimestampedPipelineHandler { - - fun update(currentPipeline: OpenCvPipeline?, currentInputSource: InputSource?) { - if(currentPipeline is TimestampedOpenCvPipeline) { - currentPipeline.setTimestamp(currentInputSource?.captureTimeNanos ?: 0L) - } - } - +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package org.openftc.easyopencv + +import com.github.serivesmejia.eocvsim.input.InputSource + +class TimestampedPipelineHandler { + + fun update(currentPipeline: OpenCvPipeline?, currentInputSource: InputSource?) { + if(currentPipeline is TimestampedOpenCvPipeline) { + currentPipeline.setTimestamp(currentInputSource?.captureTimeNanos ?: 0L) + } + } + } \ No newline at end of file diff --git a/EOCV-Sim/src/main/resources/contributors.txt b/EOCV-Sim/src/main/resources/contributors.txt index a93f3620..8823008b 100644 --- a/EOCV-Sim/src/main/resources/contributors.txt +++ b/EOCV-Sim/src/main/resources/contributors.txt @@ -1,6 +1,6 @@ -NPE & the OpenFTC Team - EOCV Developers & Advisors -serivesmejia - Main Dev -Purav - Contributor & Mac Tester -Jaran - Kotlin & Coroutines Advisor -Shaurya - Guide Contributor & Mac Tester +NPE & the OpenFTC Team - EOCV Developers & Advisors +serivesmejia - Main Dev +Purav - Contributor & Mac Tester +Jaran - Kotlin & Coroutines Advisor +Shaurya - Guide Contributor & Mac Tester Henopied - JVM Crash Bugfix \ No newline at end of file diff --git a/EOCV-Sim/src/main/resources/opensourcelibs.txt b/EOCV-Sim/src/main/resources/opensourcelibs.txt index 414e5665..c3feac3f 100644 --- a/EOCV-Sim/src/main/resources/opensourcelibs.txt +++ b/EOCV-Sim/src/main/resources/opensourcelibs.txt @@ -1,8 +1,8 @@ -OpenCV - Under Apache 2.0 License -FTC SDK - Some source code under the BSD License -EasyOpenCV - Some source code under MIT License -Gson - Under Apache 2.0 License -ClassGraph - Under MIT License -FlatLaf - Under Apache 2.0 License - +OpenCV - Under Apache 2.0 License +FTC SDK - Some source code under the BSD License +EasyOpenCV - Some source code under MIT License +Gson - Under Apache 2.0 License +ClassGraph - Under MIT License +FlatLaf - Under Apache 2.0 License + EOCV-Sim and its source code is distributed under the MIT License \ No newline at end of file diff --git a/EOCV-Sim/src/test/kotlin/com/github/serivesmejia/eocvsim/test/CoreTests.kt b/EOCV-Sim/src/test/kotlin/com/github/serivesmejia/eocvsim/test/CoreTests.kt index 655475e3..d1916747 100644 --- a/EOCV-Sim/src/test/kotlin/com/github/serivesmejia/eocvsim/test/CoreTests.kt +++ b/EOCV-Sim/src/test/kotlin/com/github/serivesmejia/eocvsim/test/CoreTests.kt @@ -1,39 +1,39 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ -@file:Suppress("UNUSED") - -package com.github.serivesmejia.eocvsim.test - -import com.github.serivesmejia.eocvsim.EOCVSim -import io.kotest.core.spec.style.StringSpec -import org.opencv.core.Mat - -class OpenCvTest : StringSpec({ - "Loading native library" { - EOCVSim.loadOpenCvLib() - } - - "Creating a Mat" { - Mat() - } +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +@file:Suppress("UNUSED") + +package com.github.serivesmejia.eocvsim.test + +import com.github.serivesmejia.eocvsim.EOCVSim +import io.kotest.core.spec.style.StringSpec +import org.opencv.core.Mat + +class OpenCvTest : StringSpec({ + "Loading native library" { + EOCVSim.loadOpenCvLib() + } + + "Creating a Mat" { + Mat() + } }) \ No newline at end of file diff --git a/NodeEye/build.gradle b/EasyVision/build.gradle similarity index 95% rename from NodeEye/build.gradle rename to EasyVision/build.gradle index 2ea0c0d3..b46563ce 100644 --- a/NodeEye/build.gradle +++ b/EasyVision/build.gradle @@ -1,9 +1,9 @@ -plugins { - id 'java' - id 'org.jetbrains.kotlin.jvm' -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib" - implementation "io.github.spair:imgui-java-app:1.84.1.0" -} +plugins { + id 'java' + id 'org.jetbrains.kotlin.jvm' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib" + implementation "io.github.spair:imgui-java-app:1.84.1.0" +} diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt new file mode 100644 index 00000000..bac8f81d --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt @@ -0,0 +1,91 @@ +package io.github.deltacv.easyvision + +import imgui.ImGui +import imgui.ImVec2 +import imgui.app.Application +import imgui.app.Configuration +import imgui.flag.ImGuiCond +import imgui.flag.ImGuiWindowFlags +import io.github.deltacv.easyvision.node.NodeEditor +import io.github.deltacv.easyvision.node.math.SumIntegerNode +import io.github.deltacv.easyvision.node.vision.InputMatNode +import io.github.deltacv.easyvision.node.vision.OutputMatNode +import org.lwjgl.BufferUtils +import org.lwjgl.glfw.GLFW +import org.lwjgl.glfw.GLFW.glfwGetWindowSize +import org.lwjgl.glfw.GLFW.glfwSetKeyCallback +import org.lwjgl.glfw.GLFWKeyCallback + +class EasyVision : Application() { + + private val w = BufferUtils.createIntBuffer(1) + private val h = BufferUtils.createIntBuffer(1) + + val windowSize: ImVec2 get() { + w.position(0) + h.position(0) + + glfwGetWindowSize(handle, w, h) + + return ImVec2(w.get(0).toFloat(), h.get(0).toFloat()) + } + + private var prevKeyCallback: GLFWKeyCallback? = null + + val editor = NodeEditor(this) + + fun start() { + editor.init() + + InputMatNode().enable() + OutputMatNode().enable() + + SumIntegerNode().enable() + SumIntegerNode().enable() + + launch(this) + + editor.destroy() + } + + override fun configure(config: Configuration) { + config.title = "EasyVision" + } + + override fun process() { + if(prevKeyCallback == null) { + // register a new key callback that will call the previous callback and handle some special keys + prevKeyCallback = glfwSetKeyCallback(handle, ::keyCallback) + } + + ImGui.setNextWindowPos(0f, 0f, ImGuiCond.Always) + + val size = windowSize + ImGui.setNextWindowSize(size.x, size.y, ImGuiCond.Always) + + ImGui.begin("Editor", + ImGuiWindowFlags.NoResize or ImGuiWindowFlags.NoMove or ImGuiWindowFlags.NoCollapse + ) + + editor.draw() + + ImGui.end() + + isDeleteReleased = false + } + + var isDeleteReleased = false + + private fun keyCallback(windowId: Long, key: Int, scancode: Int, action: Int, mods: Int) { + if(prevKeyCallback != null) { + prevKeyCallback!!.invoke(windowId, key, scancode, action, mods) //invoke the imgui callback + } + + isDeleteReleased = scancode == 119 && action == GLFW.GLFW_RELEASE + } + +} + +fun main() { + EasyVision().start() +} \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/Attribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt similarity index 80% rename from NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/Attribute.kt rename to EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt index 36094f76..4fa8574f 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/Attribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt @@ -1,42 +1,42 @@ -package io.github.deltacv.nodeeye.attribute - -import imgui.extension.imnodes.ImNodes -import io.github.deltacv.nodeeye.id.DrawableIdElement -import io.github.deltacv.nodeeye.node.Node - -enum class AttributeMode { INPUT, OUTPUT } - -abstract class Attribute : DrawableIdElement { - - abstract val mode: AttributeMode - - override val id by Node.attributes.nextId { this } - - var parentNode: Node? = null - internal set - - abstract fun drawAttribute() - - override fun draw() { - if(mode == AttributeMode.INPUT) { - ImNodes.beginInputAttribute(id) - } else { - ImNodes.beginOutputAttribute(id) - } - - drawAttribute() - - if(mode == AttributeMode.INPUT) { - ImNodes.endInputAttribute() - } else { - ImNodes.endOutputAttribute() - } - } - - override fun delete() { - Node.attributes.removeId(id) - } - - abstract fun acceptLink(other: Attribute): Boolean - +package io.github.deltacv.easyvision.attribute + +import imgui.extension.imnodes.ImNodes +import io.github.deltacv.easyvision.id.DrawableIdElement +import io.github.deltacv.easyvision.node.Node + +enum class AttributeMode { INPUT, OUTPUT } + +abstract class Attribute : DrawableIdElement { + + abstract val mode: AttributeMode + + override val id by Node.attributes.nextId { this } + + var parentNode: Node? = null + internal set + + abstract fun drawAttribute() + + override fun draw() { + if(mode == AttributeMode.INPUT) { + ImNodes.beginInputAttribute(id) + } else { + ImNodes.beginOutputAttribute(id) + } + + drawAttribute() + + if(mode == AttributeMode.INPUT) { + ImNodes.endInputAttribute() + } else { + ImNodes.endOutputAttribute() + } + } + + override fun delete() { + Node.attributes.removeId(id) + } + + abstract fun acceptLink(other: Attribute): Boolean + } \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/TextBoxAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TextBoxAttribute.kt similarity index 73% rename from NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/TextBoxAttribute.kt rename to EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TextBoxAttribute.kt index 98439a61..edda973d 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/TextBoxAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TextBoxAttribute.kt @@ -1,9 +1,9 @@ -package io.github.deltacv.nodeeye.attribute - -abstract class TextBoxAttribute(typeName: String) : TypedAttribute(typeName) { - - override fun drawAttribute() { - super.drawAttribute() - } - +package io.github.deltacv.easyvision.attribute + +abstract class TextBoxAttribute(typeName: String) : TypedAttribute(typeName) { + + override fun drawAttribute() { + super.drawAttribute() + } + } \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/TypedAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt similarity index 86% rename from NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/TypedAttribute.kt rename to EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt index 99884d13..f55bc901 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/TypedAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt @@ -1,17 +1,17 @@ -package io.github.deltacv.nodeeye.attribute - -import imgui.ImGui - -abstract class TypedAttribute(var typeName: String) : Attribute() { - - abstract var variableName: String? - - protected val finalVarName get() = variableName ?: if(mode == AttributeMode.INPUT) "Input" else "Output" - - override fun drawAttribute() { - ImGui.text("($typeName) $finalVarName") - } - - override fun acceptLink(other: Attribute) = this::class == other::class - +package io.github.deltacv.easyvision.attribute + +import imgui.ImGui + +abstract class TypedAttribute(var typeName: String) : Attribute() { + + abstract var variableName: String? + + protected val finalVarName get() = variableName ?: if(mode == AttributeMode.INPUT) "Input" else "Output" + + override fun drawAttribute() { + ImGui.text("($typeName) $finalVarName") + } + + override fun acceptLink(other: Attribute) = this::class == other::class + } \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/math/IntegerAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt similarity index 63% rename from NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/math/IntegerAttribute.kt rename to EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt index baa7ed7e..f52a98ba 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/math/IntegerAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt @@ -1,26 +1,26 @@ -package io.github.deltacv.nodeeye.attribute.math - -import imgui.ImGui -import imgui.type.ImInt -import io.github.deltacv.nodeeye.attribute.AttributeMode -import io.github.deltacv.nodeeye.attribute.TypedAttribute -import io.github.deltacv.nodeeye.node.Link.Companion.hasLink - -class IntAttribute( - override val mode: AttributeMode, - override var variableName: String? = null -) : TypedAttribute("Int") { - - val value = ImInt() - - override fun drawAttribute() { - super.drawAttribute() - - if(!hasLink && mode == AttributeMode.INPUT) { - ImGui.pushItemWidth(110.0f) - ImGui.inputInt("", value) - ImGui.popItemWidth() - } - } - +package io.github.deltacv.easyvision.attribute.math + +import imgui.ImGui +import imgui.type.ImInt +import io.github.deltacv.easyvision.attribute.AttributeMode +import io.github.deltacv.easyvision.attribute.TypedAttribute +import io.github.deltacv.easyvision.node.Link.Companion.hasLink + +class IntAttribute( + override val mode: AttributeMode, + override var variableName: String? = null +) : TypedAttribute("Int") { + + val value = ImInt() + + override fun drawAttribute() { + super.drawAttribute() + + if(!hasLink && mode == AttributeMode.INPUT) { + ImGui.pushItemWidth(110.0f) + ImGui.inputInt("", value) + ImGui.popItemWidth() + } + } + } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/MatAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/MatAttribute.kt new file mode 100644 index 00000000..6c94f49d --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/MatAttribute.kt @@ -0,0 +1,9 @@ +package io.github.deltacv.easyvision.attribute.vision + +import io.github.deltacv.easyvision.attribute.TypedAttribute +import io.github.deltacv.easyvision.attribute.AttributeMode + +class MatAttribute( + override val mode: AttributeMode, + override var variableName: String? = null +) : TypedAttribute("Image") \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/IdElement.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/id/IdElement.kt similarity index 80% rename from NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/IdElement.kt rename to EasyVision/src/main/kotlin/io/github/deltacv/easyvision/id/IdElement.kt index 4056f289..86c7c3c2 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/IdElement.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/id/IdElement.kt @@ -1,21 +1,21 @@ -package io.github.deltacv.nodeeye.id - -interface IdElement { - val id: Int -} - -interface DrawableIdElement : IdElement { - - fun draw() - - fun delete() - - fun onEnable() { } - - fun enable(): DrawableIdElement { - ::id.get() - onEnable() - return this - } - +package io.github.deltacv.easyvision.id + +interface IdElement { + val id: Int +} + +interface DrawableIdElement : IdElement { + + fun draw() + + fun delete() + + fun onEnable() { } + + fun enable(): DrawableIdElement { + ::id.get() + onEnable() + return this + } + } \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/IdElementContainer.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/id/IdElementContainer.kt similarity index 90% rename from NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/IdElementContainer.kt rename to EasyVision/src/main/kotlin/io/github/deltacv/easyvision/id/IdElementContainer.kt index f66d2cb9..f5979e9b 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/id/IdElementContainer.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/id/IdElementContainer.kt @@ -1,37 +1,37 @@ -package io.github.deltacv.nodeeye.id - -class IdElementContainer : Iterable { - - private val e = ArrayList() - - /** - * Note that the element positions in this list won't necessarily match their ids - */ - var elements = ArrayList() - private set - - fun nextId(element: () -> T) = lazy { - nextId(element()).value - } - - fun nextId(element: T) = lazy { - e.add(element) - elements.add(element) - - e.lastIndexOf(element) - } - - fun nextId() = lazy { - e.add(null) - e.lastIndexOf(null) - } - - fun removeId(id: Int) { - elements.remove(e[id]) - e[id] = null - } - - operator fun get(id: Int) = e[id] - - override fun iterator() = elements.listIterator() +package io.github.deltacv.easyvision.id + +class IdElementContainer : Iterable { + + private val e = ArrayList() + + /** + * Note that the element positions in this list won't necessarily match their ids + */ + var elements = ArrayList() + private set + + fun nextId(element: () -> T) = lazy { + nextId(element()).value + } + + fun nextId(element: T) = lazy { + e.add(element) + elements.add(element) + + e.lastIndexOf(element) + } + + fun nextId() = lazy { + e.add(null) + e.lastIndexOf(null) + } + + fun removeId(id: Int) { + elements.remove(e[id]) + e[id] = null + } + + operator fun get(id: Int) = e[id] + + override fun iterator() = elements.listIterator() } \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/DrawNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/DrawNode.kt similarity index 89% rename from NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/DrawNode.kt rename to EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/DrawNode.kt index d8734dbc..ccd94648 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/DrawNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/DrawNode.kt @@ -1,23 +1,23 @@ -package io.github.deltacv.nodeeye.node - -import imgui.ImGui -import imgui.extension.imnodes.ImNodes - -abstract class DrawNode(var title: String? = null, allowDelete: Boolean = true) : Node(allowDelete) { - - override fun draw() { - ImNodes.beginNode(id) - if(title != null) { - ImNodes.beginNodeTitleBar() - ImGui.textUnformatted(title!!) - ImNodes.endNodeTitleBar() - } - - drawNode() - drawAttributes() - ImNodes.endNode() - } - - open fun drawNode() { } - +package io.github.deltacv.easyvision.node + +import imgui.ImGui +import imgui.extension.imnodes.ImNodes + +abstract class DrawNode(var title: String? = null, allowDelete: Boolean = true) : Node(allowDelete) { + + override fun draw() { + ImNodes.beginNode(id) + if(title != null) { + ImNodes.beginNodeTitleBar() + ImGui.textUnformatted(title!!) + ImNodes.endNodeTitleBar() + } + + drawNode() + drawAttributes() + ImNodes.endNode() + } + + open fun drawNode() { } + } \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Link.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Link.kt similarity index 72% rename from NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Link.kt rename to EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Link.kt index 992b8764..df4945b7 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Link.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Link.kt @@ -1,36 +1,36 @@ -package io.github.deltacv.nodeeye.node - -import imgui.extension.imnodes.ImNodes -import io.github.deltacv.nodeeye.attribute.Attribute -import io.github.deltacv.nodeeye.id.DrawableIdElement -import io.github.deltacv.nodeeye.id.IdElementContainer - -class Link(val a: Int, val b: Int) : DrawableIdElement { - - companion object { - val links = IdElementContainer() - - fun getLinkOf(attributeId: Int): Link? { - for(link in links) { - if(link.a == attributeId || link.b == attributeId) { - return link - } - } - - return null - } - - val Attribute.hasLink get() = getLinkOf(id) != null - } - - override val id by links.nextId { this } - - override fun draw() { - ImNodes.link(id, a, b) - } - - override fun delete() { - links.removeId(id) - } - +package io.github.deltacv.easyvision.node + +import imgui.extension.imnodes.ImNodes +import io.github.deltacv.easyvision.attribute.Attribute +import io.github.deltacv.easyvision.id.DrawableIdElement +import io.github.deltacv.easyvision.id.IdElementContainer + +class Link(val a: Int, val b: Int) : DrawableIdElement { + + companion object { + val links = IdElementContainer() + + fun getLinkOf(attributeId: Int): Link? { + for(link in links) { + if(link.a == attributeId || link.b == attributeId) { + return link + } + } + + return null + } + + val Attribute.hasLink get() = getLinkOf(id) != null + } + + override val id by links.nextId { this } + + override fun draw() { + ImNodes.link(id, a, b) + } + + override fun delete() { + links.removeId(id) + } + } \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Node.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt similarity index 54% rename from NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Node.kt rename to EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt index 04df3e6d..6f5c439b 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/Node.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt @@ -1,46 +1,57 @@ -package io.github.deltacv.nodeeye.node - -import imgui.ImGui -import io.github.deltacv.nodeeye.id.DrawableIdElement -import io.github.deltacv.nodeeye.id.IdElementContainer -import io.github.deltacv.nodeeye.attribute.Attribute -import io.github.deltacv.nodeeye.attribute.AttributeMode - -abstract class Node(protected var allowDelete: Boolean = true) : DrawableIdElement { - - companion object { - val nodes = IdElementContainer() - val attributes = IdElementContainer() - - @JvmStatic protected val INPUT = AttributeMode.INPUT - @JvmStatic protected val OUTPUT = AttributeMode.OUTPUT - } - - override val id by nodes.nextId { this } - - val nodeAttributes = mutableListOf() - - protected fun drawAttributes() { - for((i, attribute) in nodeAttributes.withIndex()) { - attribute.parentNode = this - attribute.draw() - - if(i < nodeAttributes.size - 1) { - ImGui.newLine() // make a new blank line if this isn't the last attribute - } - } - } - - override fun delete() { - if(allowDelete) { - for (attribute in nodeAttributes) { - attribute.delete() - } - - nodes.removeId(id) - } - } - - operator fun Attribute.unaryPlus() = nodeAttributes.add(this) - +package io.github.deltacv.easyvision.node + +import imgui.ImGui +import io.github.deltacv.easyvision.id.DrawableIdElement +import io.github.deltacv.easyvision.id.IdElementContainer +import io.github.deltacv.easyvision.attribute.Attribute +import io.github.deltacv.easyvision.attribute.AttributeMode + +abstract class Node(protected var allowDelete: Boolean = true) : DrawableIdElement { + + companion object { + val nodes = IdElementContainer() + val attributes = IdElementContainer() + + @JvmStatic protected val INPUT = AttributeMode.INPUT + @JvmStatic protected val OUTPUT = AttributeMode.OUTPUT + } + + override val id by nodes.nextId { this } + + val nodeAttributes = mutableListOf() + + protected fun drawAttributes() { + for((i, attribute) in nodeAttributes.withIndex()) { + attribute.parentNode = this + attribute.draw() + + if(i < nodeAttributes.size - 1) { + ImGui.newLine() // make a new blank line if this isn't the last attribute + } + } + } + + override fun delete() { + if(allowDelete) { + for(link in Link.links.elements.toTypedArray()) { + for (attribute in nodeAttributes) { + if(link.a == attribute.id || link.b == attribute.id) { + // deleting links that were attached + // to any of this node's attributes + link.delete() + } + } + } + + for (attribute in nodeAttributes.toTypedArray()) { + attribute.delete() + nodeAttributes.remove(attribute) + } + + nodes.removeId(id) + } + } + + operator fun Attribute.unaryPlus() = nodeAttributes.add(this) + } \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/NodeEditor.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt similarity index 86% rename from NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/NodeEditor.kt rename to EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt index 0a84bd0e..1f4a6344 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/NodeEditor.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt @@ -1,103 +1,103 @@ -package io.github.deltacv.nodeeye.node - -import imgui.ImGui -import imgui.extension.imnodes.ImNodes -import imgui.flag.ImGuiKey -import imgui.flag.ImGuiMouseButton -import imgui.type.ImInt -import io.github.deltacv.nodeeye.attribute.AttributeMode - -class NodeEditor { - - fun init() { - ImNodes.createContext() - } - - fun draw() { - ImNodes.beginNodeEditor() - - for(node in Node.nodes) { - node.draw() - } - for(link in Link.links) { - link.draw() - } - - ImNodes.endNodeEditor() - - handleDeleteLink() - handleCreateLink() - handleDeleteSelection() - } - - private val startAttr = ImInt() - private val endAttr = ImInt() - - private fun handleCreateLink() { - if(ImNodes.isLinkCreated(startAttr, endAttr)) { - val start = startAttr.get() - val end = endAttr.get() - - val startAttrib = Node.attributes[start]!! - val endAttrib = Node.attributes[end]!! - - val input = if(startAttrib.mode == AttributeMode.INPUT) start else end - - val inputAttrib = Node.attributes[input]!! - val outputAttrib = if(startAttrib.mode == AttributeMode.OUTPUT) start else end - - if(startAttrib.mode == endAttrib.mode) { - return // linked attributes cannot be of the same mode - } - - if(!startAttrib.acceptLink(endAttrib) ||!endAttrib.acceptLink(startAttrib)) { - return // one or both of the attributes didn't accept the link, abort. - } - - if(startAttrib.parentNode == endAttrib.parentNode) { - return // we can't link a node to itself! - } - - val inputLink = Link.getLinkOf(input) - inputLink?.delete() // delete the existing link of the input attribute if there's any - - Link(start, end).enable() // create the link and enable it - } - } - - fun handleDeleteLink() { - val hoveredId = ImNodes.getHoveredLink() - - if(ImGui.isMouseClicked(ImGuiMouseButton.Right) && hoveredId >= 0) { - val hoveredLink = Link.links[hoveredId] - hoveredLink?.delete() - } - } - - fun handleDeleteSelection() { - if(ImGui.isKeyReleased(ImGuiKey.Delete)) { - if(ImNodes.numSelectedNodes() > 0) { - val selectedNodes = IntArray(ImNodes.numSelectedNodes()) - ImNodes.getSelectedNodes(selectedNodes) - - for(node in selectedNodes) { - Node.nodes[node]?.delete() - } - } - - if(ImNodes.numSelectedLinks() > 0) { - val selectedLinks = IntArray(ImNodes.numSelectedLinks()) - ImNodes.getSelectedLinks(selectedLinks) - - for(link in selectedLinks) { - Link.links[link]?.delete() - } - } - } - } - - fun destroy() { - ImNodes.destroyContext() - } - +package io.github.deltacv.easyvision.node + +import imgui.ImGui +import imgui.extension.imnodes.ImNodes +import imgui.flag.ImGuiMouseButton +import imgui.type.ImInt +import io.github.deltacv.easyvision.EasyVision +import io.github.deltacv.easyvision.attribute.AttributeMode + +class NodeEditor(val easyVision: EasyVision) { + + fun init() { + ImNodes.createContext() + } + + fun draw() { + ImNodes.beginNodeEditor() + + for(node in Node.nodes) { + node.draw() + } + for(link in Link.links) { + link.draw() + } + + ImNodes.endNodeEditor() + + handleDeleteLink() + handleCreateLink() + handleDeleteSelection() + } + + private val startAttr = ImInt() + private val endAttr = ImInt() + + private fun handleCreateLink() { + if(ImNodes.isLinkCreated(startAttr, endAttr)) { + val start = startAttr.get() + val end = endAttr.get() + + val startAttrib = Node.attributes[start]!! + val endAttrib = Node.attributes[end]!! + + val input = if(startAttrib.mode == AttributeMode.INPUT) start else end + + val inputAttrib = Node.attributes[input]!! + val outputAttrib = if(startAttrib.mode == AttributeMode.OUTPUT) start else end + + if(startAttrib.mode == endAttrib.mode) { + return // linked attributes cannot be of the same mode + } + + if(!startAttrib.acceptLink(endAttrib) ||!endAttrib.acceptLink(startAttrib)) { + return // one or both of the attributes didn't accept the link, abort. + } + + if(startAttrib.parentNode == endAttrib.parentNode) { + return // we can't link a node to itself! + } + + val inputLink = Link.getLinkOf(input) + inputLink?.delete() // delete the existing link of the input attribute if there's any + + Link(start, end).enable() // create the link and enable it + } + } + + private fun handleDeleteLink() { + val hoveredId = ImNodes.getHoveredLink() + + if(ImGui.isMouseClicked(ImGuiMouseButton.Right) && hoveredId >= 0) { + val hoveredLink = Link.links[hoveredId] + hoveredLink?.delete() + } + } + + private fun handleDeleteSelection() { + if(easyVision.isDeleteReleased) { + if(ImNodes.numSelectedNodes() > 0) { + val selectedNodes = IntArray(ImNodes.numSelectedNodes()) + ImNodes.getSelectedNodes(selectedNodes) + + for(node in selectedNodes) { + Node.nodes[node]?.delete() + } + } + + if(ImNodes.numSelectedLinks() > 0) { + val selectedLinks = IntArray(ImNodes.numSelectedLinks()) + ImNodes.getSelectedLinks(selectedLinks) + + for(link in selectedLinks) { + Link.links[link]?.delete() + } + } + } + } + + fun destroy() { + ImNodes.destroyContext() + } + } \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/math/SumIntegerNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt similarity index 53% rename from NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/math/SumIntegerNode.kt rename to EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt index a83b5ad7..7027aa60 100644 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/math/SumIntegerNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt @@ -1,15 +1,15 @@ -package io.github.deltacv.nodeeye.node.math - -import io.github.deltacv.nodeeye.node.DrawNode -import io.github.deltacv.nodeeye.attribute.math.IntAttribute - -class SumIntegerNode : DrawNode("Sum Integer") { - - override fun onEnable() { - + IntAttribute(INPUT, "A") - + IntAttribute(INPUT, "B") - - + IntAttribute(OUTPUT, "Result") - } - +package io.github.deltacv.easyvision.node.math + +import io.github.deltacv.easyvision.node.DrawNode +import io.github.deltacv.easyvision.attribute.math.IntAttribute + +class SumIntegerNode : DrawNode("Sum Integer") { + + override fun onEnable() { + + IntAttribute(INPUT, "A") + + IntAttribute(INPUT, "B") + + + IntAttribute(OUTPUT, "Result") + } + } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/InputMatNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/InputMatNode.kt new file mode 100644 index 00000000..ac138943 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/InputMatNode.kt @@ -0,0 +1,12 @@ +package io.github.deltacv.easyvision.node.vision + +import io.github.deltacv.easyvision.node.DrawNode +import io.github.deltacv.easyvision.attribute.vision.MatAttribute + +class InputMatNode : DrawNode("Pipeline Input", allowDelete = false) { + + override fun onEnable() { + + MatAttribute(OUTPUT, "Input") + } + +} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/OutputMatNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/OutputMatNode.kt new file mode 100644 index 00000000..114fa459 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/OutputMatNode.kt @@ -0,0 +1,12 @@ +package io.github.deltacv.easyvision.node.vision + +import io.github.deltacv.easyvision.node.DrawNode +import io.github.deltacv.easyvision.attribute.vision.MatAttribute + +class OutputMatNode : DrawNode("Pipeline Output", allowDelete = false) { + + override fun onEnable() { + + MatAttribute(INPUT, "Output") + } + +} \ No newline at end of file diff --git a/LICENSE b/LICENSE index c3097512..bcb08de3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2020 Sebastian Erives - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2020 Sebastian Erives + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/EasyVision.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/EasyVision.kt deleted file mode 100644 index 9ccdef31..00000000 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/EasyVision.kt +++ /dev/null @@ -1,71 +0,0 @@ -package io.github.deltacv.nodeeye - -import imgui.ImGui -import imgui.ImVec2 -import imgui.app.Application -import imgui.app.Configuration -import imgui.flag.ImGuiCond -import imgui.flag.ImGuiWindowFlags -import io.github.deltacv.nodeeye.node.* - -import io.github.deltacv.nodeeye.node.math.SumIntegerNode -import io.github.deltacv.nodeeye.node.vision.InputMatNode -import io.github.deltacv.nodeeye.node.vision.OutputMatNode - -import org.lwjgl.BufferUtils -import org.lwjgl.glfw.GLFW.glfwGetWindowSize - -class NodeEye : Application() { - - private val w = BufferUtils.createIntBuffer(1) - private val h = BufferUtils.createIntBuffer(1) - - val windowSize: ImVec2 get() { - w.position(0) - h.position(0) - - glfwGetWindowSize(handle, w, h) - - return ImVec2(w.get(0).toFloat(), h.get(0).toFloat()) - } - - val editor = NodeEditor() - - override fun configure(config: Configuration) { - config.title = "NodeEye" - } - - fun start() { - editor.init() - - InputMatNode().enable() - OutputMatNode().enable() - - SumIntegerNode().enable() - SumIntegerNode().enable() - - launch(this) - - editor.destroy() - } - - override fun process() { - ImGui.setNextWindowPos(0f, 0f, ImGuiCond.Always) - - val size = windowSize - ImGui.setNextWindowSize(size.x, size.y, ImGuiCond.Always) - - ImGui.begin("Editor", - ImGuiWindowFlags.NoResize or ImGuiWindowFlags.NoMove or ImGuiWindowFlags.NoCollapse - ) - - editor.draw() - - ImGui.end() - } - -} - -fun main() { - NodeEye().start() -} \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/vision/MatAttribute.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/vision/MatAttribute.kt deleted file mode 100644 index 0000b4cb..00000000 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/attribute/vision/MatAttribute.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.deltacv.nodeeye.attribute.vision - -import io.github.deltacv.nodeeye.attribute.TypedAttribute -import io.github.deltacv.nodeeye.attribute.AttributeMode - -class MatAttribute( - override val mode: AttributeMode, - override var variableName: String? = null -) : TypedAttribute("Image") \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/InputMatNode.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/InputMatNode.kt deleted file mode 100644 index e0e7f70a..00000000 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/InputMatNode.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.github.deltacv.nodeeye.node.vision - -import io.github.deltacv.nodeeye.node.DrawNode -import io.github.deltacv.nodeeye.attribute.vision.MatAttribute - -class InputMatNode : DrawNode("Pipeline Input", allowDelete = false) { - - override fun onEnable() { - + MatAttribute(OUTPUT, "Input") - } - -} \ No newline at end of file diff --git a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/OutputMatNode.kt b/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/OutputMatNode.kt deleted file mode 100644 index 248d02f2..00000000 --- a/NodeEye/src/main/kotlin/io/github/deltacv/nodeeye/node/vision/OutputMatNode.kt +++ /dev/null @@ -1,13 +0,0 @@ -package io.github.deltacv.nodeeye.node.vision - -import io.github.deltacv.nodeeye.node.DrawNode -import io.github.deltacv.nodeeye.attribute.AttributeMode -import io.github.deltacv.nodeeye.attribute.vision.MatAttribute - -class OutputMatNode : DrawNode("Pipeline Output", allowDelete = false) { - - override fun onEnable() { - + MatAttribute(INPUT, "Output") - } - -} \ No newline at end of file diff --git a/README.md b/README.md index 36bc046b..44105f62 100644 --- a/README.md +++ b/README.md @@ -1,308 +1,308 @@ - - -![Java CI with Gradle](https://github.com/deltacv/EOCV-Sim/workflows/Build%20and%20test%20with%20Gradle/badge.svg) -[![](https://jitpack.io/v/deltacvvesmejia/EOCV-Sim.svg)](https://jitpack.io/#deltacv/EOCV-Sim) -[![Run on Repl.it](https://repl.it/badge/github/deltacv/EOCV-Sim)](https://repl.it/github/deltacv/EOCV-Sim) - - -# Welcome! - -EOCV-Sim (EasyOpenCV Simulator) is a straightforward way to test your pipelines in a -simple user interface directly in your computer, simulating the EasyOpenCV library & a bit of -FTC SDK structure, allowing you to simply copy paste directly your pipeline code once you want to -transfer it onto your robot! - - - -### If you'd like to learn how to use the simulator, you can find a complete usage explaination [here](https://github.com/serivesmejia/EOCV-Sim/blob/master/USAGE.md) - -# Compatibility - -Since OpenCV in Java uses a native library, which is platform specific, the simulator is currently limited to the following platforms: - -* Windows x64 (tested) -* Windows x32 (untested) -* MacOS x64 (tested) -* Linux x64 (tested for Ubuntu 20.04)
- -# Installation - -1) **Download & install the Java Development Kit if you haven't already:**

- JDK 8 is the minimum required one, any JDK above that version will probably work fine.
- You can download it from [the Oracle webpage](https://www.oracle.com/java/technologies/javase-downloads.html), - and here is a [step by step video](https://www.youtube.com/watch?v=IJ-PJbvJBGs) of the installation process
- -## Recommended method - -1) **Make sure you have downloaded a JDK as mentioned above** - -2) **Go to the releases page on this repo and find the latest version ([or click here](https://github.com/deltacv/EOCV-Sim/releases/latest))** - -3) **Download the jar file, named `EOCV-Sim-X.X.X-all.jar`, available at the bottom on the "assets" section** - -4) **Choose and install an IDE/text editor**

- The recommended text editor is VS Code, with the Java Extension Pack. EOCV-Sim provides direct support for it, for creating a "VS Code Workspace" from a template, although it can also be imported into IntelliJ IDEA since it's just a normal Gradle project. - - This installation method provides the benefit of "runtime compiling", which means that the user pipelines are compiled and loaded on the fly and therefore the changes made in code can be reflected immediately, as opposed to the [old IntelliJ IDEA method](#altenative-installation-method-intellij-idea) in which the simulator had to be closed, compiled and then opened again to apply the smallest change made in a pipeline. Plus, VS Code is a lightweight editor which provides Java syntax highlighting and IntelliSense with the Java Extension Pack, making development of pipelines easy with tools like code completion. - - You can download and install VS Code from the [Visual Studio page](https://code.visualstudio.com/). The [Java Extension Pack](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack) can be installed from the [VS Code extension marketplace](https://code.visualstudio.com/docs/introvideos/extend). - - Here's a [tutorial video](https://www.youtube.com/watch?v=KwnavHTOBiA) explaining how to download and install VS Code & the Java Extension Pack - -5) **Running EOCV-Sim**

- For running the sim, simply double click the jar file downloaded from the releases page, or it can also be executed from the command line: - ```python - java -jar "EOCV-Sim-X.X.X-all.jar" - ``` - - When running on Linux (distros such as Ubuntu, Linux Mint, etc) or Unix-like secure operating systems, it might prohibit you to run it by double clicking the file from a file explorer. This can be fixed by giving execute permissions to the jar file with the following command - ```bash - chmod +x EOCV-Sim-X.X.X-all.jar - ``` - -**Now the sim should be running without any issues! If you find any problem feel free to open an issue, and check the [usage explanation](https://github.com/deltacv/EOCV-Sim/blob/master/USAGE.md) for more details about how to use the simulator (and VS Code).** - -## Altenative installation method (IntelliJ IDEA) - -No complicated setup is required for this method either, it's straight up importing the EOCV-Sim project into IntelliJ IDEA: - -\**The downside of this method is that this repo has grown to a considerable amount of space, due to a bloated history, and takes some time to clone, and also builds can be slower depending on your device.* - -1) **Make sure you have downloaded a JDK as mentioned [here](#installation)** - -2) **Download & install IntelliJ IDEA Community IDE if you haven't already:**

- You can download it from the [JetBrains webpage](https://www.jetbrains.com/idea/download/)
- Here is another great [step by step video](https://www.youtube.com/watch?v=E2okEJIbUYs) for IntelliJ installation. - -3) **Clone and import the project:**
- - 1) Open IntelliJ IDEA and in the main screen click on "Get from Version Control"
- -

- Alternatively, if you already had another project opened, go to File > New > Project from Version Control...

- - - 2) Another window will show up for cloning and importing a repository into IntelliJ
- - 1) In the "URL" field, enter: ```https://github.com/deltacv/EOCV-Sim.git```
- 2) The directory can be changed, but it will be automatically filled so it's not necessary. - 3) Make sure the "Version control" is set to "Git".

-
- 4) After that, click on the "Clone" button, located at the bottom right and the cloning process will begin...
-
- 5) After the cloning finishes, the project should automatically import and you'll have something like this:

-
- -### And you're ready to go! Refer to the [usage explanation](https://github.com/deltacv/EOCV-Sim/blob/master/USAGE.md) for further details on how to utilize the simulator.
- -## From the command-line - - 1) Clone EOCV-Sim repo and cd to the cloned folder - - git clone https://github.com/deltacv/EOCV-Sim.git - cd EOCV-Sim - \**Or it can also be manually downloaded as a ZIP file from GitHub*
- - 2) Run EOCV-Sim through gradle: - - gradlew runSim - - \**On some command lines (such as Windows PowerShell and macOS) you might need to execute "./gradlew" instead*
- -#### And that's it! You might need to wait a bit for gradle to download all the dependencies but EOCV-Sim will open eventually. - -## From repl.it - - 1) Click [here](https://repl.it/github/deltacv/EOCV-Sim) to go to repl.it, you might require to create an account if you haven't already. Once you do that, it will automatically create a new project and start cloning the EOCV-Sim repo. - - 2) After the cloning is finished, click on the green "Run" button at the top and EOCV-Sim should start. - - \**Please note that this method is not widely supported and you might run into some issues or lack of some functionality.*
- - -## Adding EOCV-Sim as a dependency - - ### Gradle - ```groovy - repositories { - maven { url 'https://jitpack.com' } //add jitpack as a maven repo - } - - dependencies { - implementation 'com.github.deltacv:EOCV-Sim:3.0.0' //add the EOCV-Sim dependency - } - ``` - - ## Maven - - Adding the jitpack maven repo - ```xml - - - jitpack.io - https://jitpack.io - - - ``` - - Adding the EOCV-Sim dependecy - ```xml - - com.github.deltacv - EOCV-Sim - 3.0.0 - - ``` - -# Contact -For any quick troubleshooting or help, you can find me on Discord as *serivesmejia#8237* and on the FTC discord server. I'll be happy to assist you in any issue you might have :)

-For bug reporting or feature requesting, use the [issues tab](https://github.com/serivesmejia/EOCV-Sim/issues) in this repository. - -# Change logs - -### [v3.0.0 - Compiling on the fly! Yay!](https://github.com/serivesmejia/EOCV-Sim/releases/tag/v3.0.0) - - - This is the 9th release for EOCV-Sim - - - Changelog: - - Runtime building! The sim now supports building pipelines on the fly, which allows for more quick and efficient testing. (Running the sim with a JDK is required for this feature to work, since normal JREs don't include a compiler to use) - - Workspaces & VS Code is the new (and recommended) way of developing pipelines. A VS Code workspace template can be created from the sim, see the usage explanation for more details. - - A file watcher was implemented, so when any modification happens in the current workspace under the "source" or "resource" folders specified in the `eocvsim_workspace.json`, a new build will be automatically triggered every 8 seconds. - - VS Code can be executed by the sim if the current system has the `code` command. It is triggered by manually opening it in the top menu bar or when creating a VS Code workspace. - - Files can now be drag and dropped into the sim to add them as Input Sources. The sim will automatically open a create dialog depending on the file extension. - - Added a "Workspace" menu under the top menu bar, which contains the new features regarding the runtime compiling. - - The UI now has smoother icons, by using a smoothing option on Java swing which makes them look a little nicer (but not much). - - Current pipeline state is now stored and reestablished if a restart happens. - - When a build is finished, the simulator tries reinitializes the currently selected pipeline if it exists, to ensure the changes were applied. Or it falls back to the `DefaultPipeline` if the old pipeline doesn't exist anymore, it also saves the state of the old pipeline and tries to apply the snapshot of the pipeline before it was reinitialized if the names of the old and new classes match. - - The sim now uses a `.eocvsim` folder under the user directory to store its files, to avoid annoying the user with unwanted files now that the runtime compiling exists and it has to store the build output somewhere. If the user has previously run an older version of eocv sim which created `eocvsim_sources.json` and/or `eocvsim_config.json` under the user home directory, it automatically migrates them to the new folder. - - Builds created by IntelliJ Idea (the common programming style) are now considered as "dev". This helps to distinguish between official & published builds created in a CI workflow and local builds, when an issue happens and it's reported. - - The sim compiling target was changed back to Java 8, since this is one of the most widely used versions and we weren't really using many Java 9 features that couldn't have been replaced or handle different. This is also more convenient and provides better support for users directly downloading the jar and executing it. - - - Bugfixes: - - Fixed issues with the source selector regarding to selection when a modification or error happens. When a new source is added, it's automatically selected. And when a source is deleted, the previous source in the list is selected - - Fixed the color picker cursor size on non-windows systems - - Fixed pause not working when a tunable field that uses a combo box in the UI is included. - - Fixed an (apparently random, but it's just the garbage collector being weird) null pointer exception with `Enum` fields - - - Internals: - - Improved event handlers to be more idiomatic and less weird. Bye bye KEventListener! - - Improved some messy parts of the internal code and logic - -### [v2.2.1 - JVM crashing hotfix](https://github.com/serivesmejia/releases/tag/v2.2.0) - - - This is the 8th release for EOCV-Sim - - - Changelog: - - Removed "Java memory" message in the title since it's practically useless for the end user - - Updated to Gradle 7.0 for Java 16+ support (#25) - - - Bugfixes: - - Fixed JVM crashing error caused by releasing all mats in a MatRecycler finalization (#26) - - Improved memory usage by deleting unused BufferedImageRecyclers, memory is now slightly freed when allocating or recycling buffered images of different sizes (which means that the memory usage is reduced a little bit when zooming in the viewport) - -### [v2.2.0 - Variable Tuner Upgrade](https://github.com/serivesmejia/releases/tag/v2.2.0) - - - This is the 7th release for EOCV-Sim - - - Changelog: - - - Pipelines now have a timeout all of the three methods so that the main loop doesn't get compromised due to a "stuck" pipeline (using kotlin coroutines) - - processFrame has a timeout of 4.2 seconds - - init is executed in the same scope as processFrame, when it has to be called, the timeout is doubled (16.4) - - When either processFrame or init methods timeout, the sim automatically falls back to the default pipeline and discards any frame that the old timeouted pipeline could return. - - onViewportTapped is still called from the U.I Thread, but it now has a timeout of 4.2 seconds too - - Added EnumField which handles the type Enum (accepts all classes of type enum, including the ones declared by the user) - - Major improvements to the variable tuner, added new features for color picking, tuning with sliders, configuration... See [usage explanation](https://github.com/serivesmejia/EOCV-Sim/blob/master/USAGE.md) for further details. - - GUI improvement: Dropped some external dialogs in favor of simple "popups" for more practicality - - Internals: - - Continued rewrite to kotlin - - Splitted visualizer class components into different classes - - Improved EventHandler doOnce listeners - -### [v2.1.0 - Video Update](https://github.com/serivesmejia/EOCV-Sim/releases/tag/v2.1.0) - - - This is the 6th release for EOCV-Sim - - - Changelog: - - - Added support for VideoSources! You can now input your pipeline with a moving video (*.avi format is the most supported and tested, other codecs might depend on the OS you're using) - - Added support for video recording, accessible at the bottom of the pipeline selector. Save format is AVI - - Added a new TunableField type: RectField, which handles the OpenCV type "Rect" (might be useful for rect pipelines 👀) - - Improved uncaught exception handling and added a crash report generator - - Added support for more themes from FlatLaf - - Added new config option to change the output video recording size - - Added support for EOCV's TimestampedOpenCvPipeline - - Internals: - - Major rewrite to kotlin! (Still mostly Java but that might change soon) - - A bit of code cleaning and restructuring - -### [v2.0.2 - TaskBar hotfix](https://github.com/serivesmejia/EOCV-Sim/releases/tag/v2.0.2) - - - This is the 5th release for EOCV-Sim. - - - Bugfixes: - - - Fixes UnsupportedOperationException with the TaskBar API in some operating system - -### [v2.0.1 - BooleanField hotfix](https://github.com/serivesmejia/EOCV-Sim/releases/tag/v2.0.1) - - - This is the 4th release for EOCV-Sim. - - - Bugfixes: - - - Fixes ArrayIndexOutOfBoundsException when initial value of a boolean field was true which would make the sim enter into a frozen state. - -### [v2.0.0 - Major Update](https://github.com/serivesmejia/EOCV-Sim/releases/tag/v2.0.0) - - - This is the 3rd release for EOCV-Sim. - - - Changelog: - - - Gradle is now used as the main build system - - Added variable tuner for public non-final supported fields in the pipeline, accessible on the bottom part of the image viewport. - - Pipeline pause and resume option to save resources, pauses automatically with image sources for one-shot analysis - - Top Menu bar containing new features/convenient shortcuts: - - Save Mat to disk option in File submenu - - Restart feature in File submenu - - Shortcut for creating input sources under File -> New -> Input Source - - Settings menu under Edit submenu - - "About" information screen under Help submenu - - Appereance themes via the FlatLaf library, selectable in the settings window - - Telemetry now is passed to the pipeline via the constructor rather than an instance variable, check usage explaination for further details - - Mat visualizing into the viewport is now handled in another thread to improve performance - - Pipeline FPS are now capped at 30 - - Zooming viewport is now supported, using mouse wheel while holding Ctrl key - - - Bugfixes: - - - Removed call to the gc in the main loop due to performance issues - - Fixed BufferedImage mem leak by recycling previously used buffered images and trying to flush them - - Some internal code cleaning & reestructuration - - Fixed issues with native lib loading (mostly on Mac) with the OpenCV package provided by OpenPnP - -### [v1.1.0 - Telemetry Update](https://github.com/serivesmejia/EOCV-Sim/releases/tag/v1.1.0) - - - This is the 2rd release for EOCV-Sim. - - - Changelog: - - - Added a Telemetry implementation displayed in the UI. Replicates the FTC SDK one, it can be used directly in pipelines. - - Added an option to define the CameraSource resolution when creation. - - Added MacOS support (thnx Noah) - - Changed default resolution to 320x280 everywhere since it is the most commonly used in EOCV - - Native libs are now downloaded by the simulator from another GitHub repo to avoid bloating the repository with heavy files - - Java libraries, such as classgraph, opencv and gson are now delivered in compiled jars to improve compile times - - - Bug fixes: - - - Fixed a bug where the InputSources would return a BGR Mat instead of RGB, which is the type EOCV gives. - - Regarding the last point, the visualizer now expects for the given mats to be RGB - - Improved general IO error handling everywhere, from file accessing to input sources reading, so that the simulator doesn’t enter in a freeze state if any IO related operation fails - - Improved multi threading handling for changing pipelines and inputsources. - - Fixed issue in Linux where the buttons would be moved to an incorrect position when resizing out and then trying to resize back to the original size - - -### [v1.0.0 - Initial Release](https://github.com/serivesmejia/EOCV-Sim/releases/tag/v1.0.0) - - - Initial EOCV-Sim release. - + + +![Java CI with Gradle](https://github.com/deltacv/EOCV-Sim/workflows/Build%20and%20test%20with%20Gradle/badge.svg) +[![](https://jitpack.io/v/deltacvvesmejia/EOCV-Sim.svg)](https://jitpack.io/#deltacv/EOCV-Sim) +[![Run on Repl.it](https://repl.it/badge/github/deltacv/EOCV-Sim)](https://repl.it/github/deltacv/EOCV-Sim) + + +# Welcome! + +EOCV-Sim (EasyOpenCV Simulator) is a straightforward way to test your pipelines in a +simple user interface directly in your computer, simulating the EasyOpenCV library & a bit of +FTC SDK structure, allowing you to simply copy paste directly your pipeline code once you want to +transfer it onto your robot! + + + +### If you'd like to learn how to use the simulator, you can find a complete usage explaination [here](https://github.com/serivesmejia/EOCV-Sim/blob/master/USAGE.md) + +# Compatibility + +Since OpenCV in Java uses a native library, which is platform specific, the simulator is currently limited to the following platforms: + +* Windows x64 (tested) +* Windows x32 (untested) +* MacOS x64 (tested) +* Linux x64 (tested for Ubuntu 20.04)
+ +# Installation + +1) **Download & install the Java Development Kit if you haven't already:**

+ JDK 8 is the minimum required one, any JDK above that version will probably work fine.
+ You can download it from [the Oracle webpage](https://www.oracle.com/java/technologies/javase-downloads.html), + and here is a [step by step video](https://www.youtube.com/watch?v=IJ-PJbvJBGs) of the installation process
+ +## Recommended method + +1) **Make sure you have downloaded a JDK as mentioned above** + +2) **Go to the releases page on this repo and find the latest version ([or click here](https://github.com/deltacv/EOCV-Sim/releases/latest))** + +3) **Download the jar file, named `EOCV-Sim-X.X.X-all.jar`, available at the bottom on the "assets" section** + +4) **Choose and install an IDE/text editor**

+ The recommended text editor is VS Code, with the Java Extension Pack. EOCV-Sim provides direct support for it, for creating a "VS Code Workspace" from a template, although it can also be imported into IntelliJ IDEA since it's just a normal Gradle project. + + This installation method provides the benefit of "runtime compiling", which means that the user pipelines are compiled and loaded on the fly and therefore the changes made in code can be reflected immediately, as opposed to the [old IntelliJ IDEA method](#altenative-installation-method-intellij-idea) in which the simulator had to be closed, compiled and then opened again to apply the smallest change made in a pipeline. Plus, VS Code is a lightweight editor which provides Java syntax highlighting and IntelliSense with the Java Extension Pack, making development of pipelines easy with tools like code completion. + + You can download and install VS Code from the [Visual Studio page](https://code.visualstudio.com/). The [Java Extension Pack](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack) can be installed from the [VS Code extension marketplace](https://code.visualstudio.com/docs/introvideos/extend). + + Here's a [tutorial video](https://www.youtube.com/watch?v=KwnavHTOBiA) explaining how to download and install VS Code & the Java Extension Pack + +5) **Running EOCV-Sim**

+ For running the sim, simply double click the jar file downloaded from the releases page, or it can also be executed from the command line: + ```python + java -jar "EOCV-Sim-X.X.X-all.jar" + ``` + + When running on Linux (distros such as Ubuntu, Linux Mint, etc) or Unix-like secure operating systems, it might prohibit you to run it by double clicking the file from a file explorer. This can be fixed by giving execute permissions to the jar file with the following command + ```bash + chmod +x EOCV-Sim-X.X.X-all.jar + ``` + +**Now the sim should be running without any issues! If you find any problem feel free to open an issue, and check the [usage explanation](https://github.com/deltacv/EOCV-Sim/blob/master/USAGE.md) for more details about how to use the simulator (and VS Code).** + +## Altenative installation method (IntelliJ IDEA) + +No complicated setup is required for this method either, it's straight up importing the EOCV-Sim project into IntelliJ IDEA: + +\**The downside of this method is that this repo has grown to a considerable amount of space, due to a bloated history, and takes some time to clone, and also builds can be slower depending on your device.* + +1) **Make sure you have downloaded a JDK as mentioned [here](#installation)** + +2) **Download & install IntelliJ IDEA Community IDE if you haven't already:**

+ You can download it from the [JetBrains webpage](https://www.jetbrains.com/idea/download/)
+ Here is another great [step by step video](https://www.youtube.com/watch?v=E2okEJIbUYs) for IntelliJ installation. + +3) **Clone and import the project:**
+ + 1) Open IntelliJ IDEA and in the main screen click on "Get from Version Control"
+ +

+ Alternatively, if you already had another project opened, go to File > New > Project from Version Control...

+ + + 2) Another window will show up for cloning and importing a repository into IntelliJ
+ + 1) In the "URL" field, enter: ```https://github.com/deltacv/EOCV-Sim.git```
+ 2) The directory can be changed, but it will be automatically filled so it's not necessary. + 3) Make sure the "Version control" is set to "Git".

+
+ 4) After that, click on the "Clone" button, located at the bottom right and the cloning process will begin...
+
+ 5) After the cloning finishes, the project should automatically import and you'll have something like this:

+
+ +### And you're ready to go! Refer to the [usage explanation](https://github.com/deltacv/EOCV-Sim/blob/master/USAGE.md) for further details on how to utilize the simulator.
+ +## From the command-line + + 1) Clone EOCV-Sim repo and cd to the cloned folder + + git clone https://github.com/deltacv/EOCV-Sim.git + cd EOCV-Sim + \**Or it can also be manually downloaded as a ZIP file from GitHub*
+ + 2) Run EOCV-Sim through gradle: + + gradlew runSim + + \**On some command lines (such as Windows PowerShell and macOS) you might need to execute "./gradlew" instead*
+ +#### And that's it! You might need to wait a bit for gradle to download all the dependencies but EOCV-Sim will open eventually. + +## From repl.it + + 1) Click [here](https://repl.it/github/deltacv/EOCV-Sim) to go to repl.it, you might require to create an account if you haven't already. Once you do that, it will automatically create a new project and start cloning the EOCV-Sim repo. + + 2) After the cloning is finished, click on the green "Run" button at the top and EOCV-Sim should start. + + \**Please note that this method is not widely supported and you might run into some issues or lack of some functionality.*
+ + +## Adding EOCV-Sim as a dependency + + ### Gradle + ```groovy + repositories { + maven { url 'https://jitpack.com' } //add jitpack as a maven repo + } + + dependencies { + implementation 'com.github.deltacv:EOCV-Sim:3.0.0' //add the EOCV-Sim dependency + } + ``` + + ## Maven + + Adding the jitpack maven repo + ```xml + + + jitpack.io + https://jitpack.io + + + ``` + + Adding the EOCV-Sim dependecy + ```xml + + com.github.deltacv + EOCV-Sim + 3.0.0 + + ``` + +# Contact +For any quick troubleshooting or help, you can find me on Discord as *serivesmejia#8237* and on the FTC discord server. I'll be happy to assist you in any issue you might have :)

+For bug reporting or feature requesting, use the [issues tab](https://github.com/serivesmejia/EOCV-Sim/issues) in this repository. + +# Change logs + +### [v3.0.0 - Compiling on the fly! Yay!](https://github.com/serivesmejia/EOCV-Sim/releases/tag/v3.0.0) + + - This is the 9th release for EOCV-Sim + + - Changelog: + - Runtime building! The sim now supports building pipelines on the fly, which allows for more quick and efficient testing. (Running the sim with a JDK is required for this feature to work, since normal JREs don't include a compiler to use) + - Workspaces & VS Code is the new (and recommended) way of developing pipelines. A VS Code workspace template can be created from the sim, see the usage explanation for more details. + - A file watcher was implemented, so when any modification happens in the current workspace under the "source" or "resource" folders specified in the `eocvsim_workspace.json`, a new build will be automatically triggered every 8 seconds. + - VS Code can be executed by the sim if the current system has the `code` command. It is triggered by manually opening it in the top menu bar or when creating a VS Code workspace. + - Files can now be drag and dropped into the sim to add them as Input Sources. The sim will automatically open a create dialog depending on the file extension. + - Added a "Workspace" menu under the top menu bar, which contains the new features regarding the runtime compiling. + - The UI now has smoother icons, by using a smoothing option on Java swing which makes them look a little nicer (but not much). + - Current pipeline state is now stored and reestablished if a restart happens. + - When a build is finished, the simulator tries reinitializes the currently selected pipeline if it exists, to ensure the changes were applied. Or it falls back to the `DefaultPipeline` if the old pipeline doesn't exist anymore, it also saves the state of the old pipeline and tries to apply the snapshot of the pipeline before it was reinitialized if the names of the old and new classes match. + - The sim now uses a `.eocvsim` folder under the user directory to store its files, to avoid annoying the user with unwanted files now that the runtime compiling exists and it has to store the build output somewhere. If the user has previously run an older version of eocv sim which created `eocvsim_sources.json` and/or `eocvsim_config.json` under the user home directory, it automatically migrates them to the new folder. + - Builds created by IntelliJ Idea (the common programming style) are now considered as "dev". This helps to distinguish between official & published builds created in a CI workflow and local builds, when an issue happens and it's reported. + - The sim compiling target was changed back to Java 8, since this is one of the most widely used versions and we weren't really using many Java 9 features that couldn't have been replaced or handle different. This is also more convenient and provides better support for users directly downloading the jar and executing it. + + - Bugfixes: + - Fixed issues with the source selector regarding to selection when a modification or error happens. When a new source is added, it's automatically selected. And when a source is deleted, the previous source in the list is selected + - Fixed the color picker cursor size on non-windows systems + - Fixed pause not working when a tunable field that uses a combo box in the UI is included. + - Fixed an (apparently random, but it's just the garbage collector being weird) null pointer exception with `Enum` fields + + - Internals: + - Improved event handlers to be more idiomatic and less weird. Bye bye KEventListener! + - Improved some messy parts of the internal code and logic + +### [v2.2.1 - JVM crashing hotfix](https://github.com/serivesmejia/releases/tag/v2.2.0) + + - This is the 8th release for EOCV-Sim + + - Changelog: + - Removed "Java memory" message in the title since it's practically useless for the end user + - Updated to Gradle 7.0 for Java 16+ support (#25) + + - Bugfixes: + - Fixed JVM crashing error caused by releasing all mats in a MatRecycler finalization (#26) + - Improved memory usage by deleting unused BufferedImageRecyclers, memory is now slightly freed when allocating or recycling buffered images of different sizes (which means that the memory usage is reduced a little bit when zooming in the viewport) + +### [v2.2.0 - Variable Tuner Upgrade](https://github.com/serivesmejia/releases/tag/v2.2.0) + + - This is the 7th release for EOCV-Sim + + - Changelog: + + - Pipelines now have a timeout all of the three methods so that the main loop doesn't get compromised due to a "stuck" pipeline (using kotlin coroutines) + - processFrame has a timeout of 4.2 seconds + - init is executed in the same scope as processFrame, when it has to be called, the timeout is doubled (16.4) + - When either processFrame or init methods timeout, the sim automatically falls back to the default pipeline and discards any frame that the old timeouted pipeline could return. + - onViewportTapped is still called from the U.I Thread, but it now has a timeout of 4.2 seconds too + - Added EnumField which handles the type Enum (accepts all classes of type enum, including the ones declared by the user) + - Major improvements to the variable tuner, added new features for color picking, tuning with sliders, configuration... See [usage explanation](https://github.com/serivesmejia/EOCV-Sim/blob/master/USAGE.md) for further details. + - GUI improvement: Dropped some external dialogs in favor of simple "popups" for more practicality + - Internals: + - Continued rewrite to kotlin + - Splitted visualizer class components into different classes + - Improved EventHandler doOnce listeners + +### [v2.1.0 - Video Update](https://github.com/serivesmejia/EOCV-Sim/releases/tag/v2.1.0) + + - This is the 6th release for EOCV-Sim + + - Changelog: + + - Added support for VideoSources! You can now input your pipeline with a moving video (*.avi format is the most supported and tested, other codecs might depend on the OS you're using) + - Added support for video recording, accessible at the bottom of the pipeline selector. Save format is AVI + - Added a new TunableField type: RectField, which handles the OpenCV type "Rect" (might be useful for rect pipelines 👀) + - Improved uncaught exception handling and added a crash report generator + - Added support for more themes from FlatLaf + - Added new config option to change the output video recording size + - Added support for EOCV's TimestampedOpenCvPipeline + - Internals: + - Major rewrite to kotlin! (Still mostly Java but that might change soon) + - A bit of code cleaning and restructuring + +### [v2.0.2 - TaskBar hotfix](https://github.com/serivesmejia/EOCV-Sim/releases/tag/v2.0.2) + + - This is the 5th release for EOCV-Sim. + + - Bugfixes: + + - Fixes UnsupportedOperationException with the TaskBar API in some operating system + +### [v2.0.1 - BooleanField hotfix](https://github.com/serivesmejia/EOCV-Sim/releases/tag/v2.0.1) + + - This is the 4th release for EOCV-Sim. + + - Bugfixes: + + - Fixes ArrayIndexOutOfBoundsException when initial value of a boolean field was true which would make the sim enter into a frozen state. + +### [v2.0.0 - Major Update](https://github.com/serivesmejia/EOCV-Sim/releases/tag/v2.0.0) + + - This is the 3rd release for EOCV-Sim. + + - Changelog: + + - Gradle is now used as the main build system + - Added variable tuner for public non-final supported fields in the pipeline, accessible on the bottom part of the image viewport. + - Pipeline pause and resume option to save resources, pauses automatically with image sources for one-shot analysis + - Top Menu bar containing new features/convenient shortcuts: + - Save Mat to disk option in File submenu + - Restart feature in File submenu + - Shortcut for creating input sources under File -> New -> Input Source + - Settings menu under Edit submenu + - "About" information screen under Help submenu + - Appereance themes via the FlatLaf library, selectable in the settings window + - Telemetry now is passed to the pipeline via the constructor rather than an instance variable, check usage explaination for further details + - Mat visualizing into the viewport is now handled in another thread to improve performance + - Pipeline FPS are now capped at 30 + - Zooming viewport is now supported, using mouse wheel while holding Ctrl key + + - Bugfixes: + + - Removed call to the gc in the main loop due to performance issues + - Fixed BufferedImage mem leak by recycling previously used buffered images and trying to flush them + - Some internal code cleaning & reestructuration + - Fixed issues with native lib loading (mostly on Mac) with the OpenCV package provided by OpenPnP + +### [v1.1.0 - Telemetry Update](https://github.com/serivesmejia/EOCV-Sim/releases/tag/v1.1.0) + + - This is the 2rd release for EOCV-Sim. + + - Changelog: + + - Added a Telemetry implementation displayed in the UI. Replicates the FTC SDK one, it can be used directly in pipelines. + - Added an option to define the CameraSource resolution when creation. + - Added MacOS support (thnx Noah) + - Changed default resolution to 320x280 everywhere since it is the most commonly used in EOCV + - Native libs are now downloaded by the simulator from another GitHub repo to avoid bloating the repository with heavy files + - Java libraries, such as classgraph, opencv and gson are now delivered in compiled jars to improve compile times + + - Bug fixes: + + - Fixed a bug where the InputSources would return a BGR Mat instead of RGB, which is the type EOCV gives. + - Regarding the last point, the visualizer now expects for the given mats to be RGB + - Improved general IO error handling everywhere, from file accessing to input sources reading, so that the simulator doesn’t enter in a freeze state if any IO related operation fails + - Improved multi threading handling for changing pipelines and inputsources. + - Fixed issue in Linux where the buttons would be moved to an incorrect position when resizing out and then trying to resize back to the original size + + +### [v1.0.0 - Initial Release](https://github.com/serivesmejia/EOCV-Sim/releases/tag/v1.0.0) + + - Initial EOCV-Sim release. + diff --git a/TeamCode/build.gradle b/TeamCode/build.gradle index 7d523cbb..cf763982 100644 --- a/TeamCode/build.gradle +++ b/TeamCode/build.gradle @@ -1,18 +1,18 @@ -plugins { - id 'java' - id 'org.jetbrains.kotlin.jvm' -} - -apply from: '../build.common.gradle' - -dependencies { - implementation 'org.openpnp:opencv:4.3.0-2' - implementation project(':EOCV-Sim') - - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" -} - -task(runSim, dependsOn: 'classes', type: JavaExec) { - main = 'com.github.serivesmejia.eocvsim.Main' - classpath = sourceSets.main.runtimeClasspath +plugins { + id 'java' + id 'org.jetbrains.kotlin.jvm' +} + +apply from: '../build.common.gradle' + +dependencies { + implementation 'org.openpnp:opencv:4.3.0-2' + implementation project(':EOCV-Sim') + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" +} + +task(runSim, dependsOn: 'classes', type: JavaExec) { + main = 'com.github.serivesmejia.eocvsim.Main' + classpath = sourceSets.main.runtimeClasspath } \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SimpleThresholdPipeline.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SimpleThresholdPipeline.java index 14b27e0e..c68d0858 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SimpleThresholdPipeline.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SimpleThresholdPipeline.java @@ -1,171 +1,171 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package org.firstinspires.ftc.teamcode; - -import org.firstinspires.ftc.robotcore.external.Telemetry; -import org.opencv.core.Core; -import org.opencv.core.Mat; -import org.opencv.core.Scalar; -import org.opencv.imgproc.Imgproc; -import org.openftc.easyopencv.OpenCvPipeline; - -public class SimpleThresholdPipeline extends OpenCvPipeline { - - /* - * These are our variables that will be - * modifiable from the variable tuner. - * - * Scalars in OpenCV are generally used to - * represent color. So our values in the - * lower and upper Scalars here represent - * the Y, Cr and Cb values respectively. - * - * YCbCr, like most color spaces, range - * from 0-255, so we default to those - * min and max values here for now, meaning - * that all pixels will be shown. - */ - public Scalar lower = new Scalar(0, 0, 0); - public Scalar upper = new Scalar(255, 255, 255); - - /** - * This will allow us to choose the color - * space we want to use on the live field - * tuner instead of hardcoding it - */ - public ColorSpace colorSpace = ColorSpace.YCrCb; - - /* - * A good practice when typing EOCV pipelines is - * declaring the Mats you will use here at the top - * of your pipeline, to reuse the same buffers every - * time. This removes the need to call mat.release() - * with every Mat you create on the processFrame method, - * and therefore, reducing the possibility of getting a - * memory leak and causing the app to crash due to an - * "Out of Memory" error. - */ - private Mat ycrcbMat = new Mat(); - private Mat binaryMat = new Mat(); - private Mat maskedInputMat = new Mat(); - - private Telemetry telemetry = null; - - /** - * Enum to choose which color space to choose - * with the live variable tuner isntead of - * hardcoding it. - */ - enum ColorSpace { - /* - * Define our "conversion codes" in the enum - * so that we don't have to do a switch - * statement in the processFrame method. - */ - RGB(Imgproc.COLOR_RGBA2RGB), - HSV(Imgproc.COLOR_RGB2HSV), - YCrCb(Imgproc.COLOR_RGB2YCrCb), - Lab(Imgproc.COLOR_RGB2Lab); - - //store cvtCode in a public var - public int cvtCode = 0; - - //constructor to be used by enum declarations above - ColorSpace(int cvtCode) { - this.cvtCode = cvtCode; - } - } - - public SimpleThresholdPipeline(Telemetry telemetry) { - this.telemetry = telemetry; - } - - @Override - public Mat processFrame(Mat input) { - /* - * Converts our input mat from RGB to - * specified color space by the enum. - * EOCV ALWAYS returns RGB mats, so you'd - * always convert from RGB to the color - * space you want to use. - * - * Takes our "input" mat as an input, and outputs - * to a separate Mat buffer "ycrcbMat" - */ - Imgproc.cvtColor(input, ycrcbMat, colorSpace.cvtCode); - - /* - * This is where our thresholding actually happens. - * Takes our "ycrcbMat" as input and outputs a "binary" - * Mat to "binaryMat" of the same size as our input. - * "Discards" all the pixels outside the bounds specified - * by the scalars above (and modifiable with EOCV-Sim's - * live variable tuner.) - * - * Binary meaning that we have either a 0 or 255 value - * for every pixel. - * - * 0 represents our pixels that were outside the bounds - * 255 represents our pixels that are inside the bounds - */ - Core.inRange(ycrcbMat, lower, upper, binaryMat); - - /* - * Release the reusable Mat so that old data doesn't - * affect the next step in the current processing - */ - maskedInputMat.release(); - - /* - * Now, with our binary Mat, we perform a "bitwise and" - * to our input image, meaning that we will perform a mask - * which will include the pixels from our input Mat which - * are "255" in our binary Mat (meaning that they're inside - * the range) and will discard any other pixel outside the - * range (RGB 0, 0, 0. All discarded pixels will be black) - */ - Core.bitwise_and(input, input, maskedInputMat, binaryMat); - - /** - * Add some nice and informative telemetry messages - */ - telemetry.addData("[>]", "Change these values in tuner menu"); - telemetry.addData("[Color Space]", colorSpace.name()); - telemetry.addData("[Lower Scalar]", lower); - telemetry.addData("[Upper Scalar]", upper); - telemetry.update(); - - /* - * The Mat returned from this method is the - * one displayed on the viewport. - * - * To visualize our threshold, we'll return - * the "masked input mat" which shows the - * pixel from the input Mat that were inside - * the threshold range. - */ - return maskedInputMat; - } - -} +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package org.firstinspires.ftc.teamcode; + +import org.firstinspires.ftc.robotcore.external.Telemetry; +import org.opencv.core.Core; +import org.opencv.core.Mat; +import org.opencv.core.Scalar; +import org.opencv.imgproc.Imgproc; +import org.openftc.easyopencv.OpenCvPipeline; + +public class SimpleThresholdPipeline extends OpenCvPipeline { + + /* + * These are our variables that will be + * modifiable from the variable tuner. + * + * Scalars in OpenCV are generally used to + * represent color. So our values in the + * lower and upper Scalars here represent + * the Y, Cr and Cb values respectively. + * + * YCbCr, like most color spaces, range + * from 0-255, so we default to those + * min and max values here for now, meaning + * that all pixels will be shown. + */ + public Scalar lower = new Scalar(0, 0, 0); + public Scalar upper = new Scalar(255, 255, 255); + + /** + * This will allow us to choose the color + * space we want to use on the live field + * tuner instead of hardcoding it + */ + public ColorSpace colorSpace = ColorSpace.YCrCb; + + /* + * A good practice when typing EOCV pipelines is + * declaring the Mats you will use here at the top + * of your pipeline, to reuse the same buffers every + * time. This removes the need to call mat.release() + * with every Mat you create on the processFrame method, + * and therefore, reducing the possibility of getting a + * memory leak and causing the app to crash due to an + * "Out of Memory" error. + */ + private Mat ycrcbMat = new Mat(); + private Mat binaryMat = new Mat(); + private Mat maskedInputMat = new Mat(); + + private Telemetry telemetry = null; + + /** + * Enum to choose which color space to choose + * with the live variable tuner isntead of + * hardcoding it. + */ + enum ColorSpace { + /* + * Define our "conversion codes" in the enum + * so that we don't have to do a switch + * statement in the processFrame method. + */ + RGB(Imgproc.COLOR_RGBA2RGB), + HSV(Imgproc.COLOR_RGB2HSV), + YCrCb(Imgproc.COLOR_RGB2YCrCb), + Lab(Imgproc.COLOR_RGB2Lab); + + //store cvtCode in a public var + public int cvtCode = 0; + + //constructor to be used by enum declarations above + ColorSpace(int cvtCode) { + this.cvtCode = cvtCode; + } + } + + public SimpleThresholdPipeline(Telemetry telemetry) { + this.telemetry = telemetry; + } + + @Override + public Mat processFrame(Mat input) { + /* + * Converts our input mat from RGB to + * specified color space by the enum. + * EOCV ALWAYS returns RGB mats, so you'd + * always convert from RGB to the color + * space you want to use. + * + * Takes our "input" mat as an input, and outputs + * to a separate Mat buffer "ycrcbMat" + */ + Imgproc.cvtColor(input, ycrcbMat, colorSpace.cvtCode); + + /* + * This is where our thresholding actually happens. + * Takes our "ycrcbMat" as input and outputs a "binary" + * Mat to "binaryMat" of the same size as our input. + * "Discards" all the pixels outside the bounds specified + * by the scalars above (and modifiable with EOCV-Sim's + * live variable tuner.) + * + * Binary meaning that we have either a 0 or 255 value + * for every pixel. + * + * 0 represents our pixels that were outside the bounds + * 255 represents our pixels that are inside the bounds + */ + Core.inRange(ycrcbMat, lower, upper, binaryMat); + + /* + * Release the reusable Mat so that old data doesn't + * affect the next step in the current processing + */ + maskedInputMat.release(); + + /* + * Now, with our binary Mat, we perform a "bitwise and" + * to our input image, meaning that we will perform a mask + * which will include the pixels from our input Mat which + * are "255" in our binary Mat (meaning that they're inside + * the range) and will discard any other pixel outside the + * range (RGB 0, 0, 0. All discarded pixels will be black) + */ + Core.bitwise_and(input, input, maskedInputMat, binaryMat); + + /** + * Add some nice and informative telemetry messages + */ + telemetry.addData("[>]", "Change these values in tuner menu"); + telemetry.addData("[Color Space]", colorSpace.name()); + telemetry.addData("[Lower Scalar]", lower); + telemetry.addData("[Upper Scalar]", upper); + telemetry.update(); + + /* + * The Mat returned from this method is the + * one displayed on the viewport. + * + * To visualize our threshold, we'll return + * the "masked input mat" which shows the + * pixel from the input Mat that were inside + * the threshold range. + */ + return maskedInputMat; + } + +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SkystoneDeterminationPipeline.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SkystoneDeterminationPipeline.java index cc77c245..c7e92500 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SkystoneDeterminationPipeline.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SkystoneDeterminationPipeline.java @@ -1,308 +1,308 @@ -/* - * Copyright (c) 2020 OpenFTC Team - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.firstinspires.ftc.teamcode; - -import org.firstinspires.ftc.robotcore.external.Telemetry; -import org.opencv.core.Core; -import org.opencv.core.Mat; -import org.opencv.core.Point; -import org.opencv.core.Rect; -import org.opencv.core.Scalar; -import org.opencv.imgproc.Imgproc; -import org.openftc.easyopencv.OpenCvPipeline; - -public class SkystoneDeterminationPipeline extends OpenCvPipeline { - /* - * An enum to define the skystone position - */ - public enum SkystonePosition - { - LEFT, - CENTER, - RIGHT - } - - /* - * Some color constants - */ - public final Scalar BLUE = new Scalar(0, 0, 255); - public final Scalar GREEN = new Scalar(0, 255, 0); - - /* - * The core values which define the location and size of the sample regions - */ - static final Point REGION1_TOPLEFT_ANCHOR_POINT = new Point(109,98); - static final Point REGION2_TOPLEFT_ANCHOR_POINT = new Point(181,98); - static final Point REGION3_TOPLEFT_ANCHOR_POINT = new Point(253,98); - static final int REGION_WIDTH = 20; - static final int REGION_HEIGHT = 20; - - /* - * Points which actually define the sample region rectangles, derived from above values - * - * Example of how points A and B work to define a rectangle - * - * ------------------------------------ - * | (0,0) Point A | - * | | - * | | - * | | - * | | - * | | - * | | - * | Point B (70,50) | - * ------------------------------------ - * - */ - Point region1_pointA = new Point( - REGION1_TOPLEFT_ANCHOR_POINT.x, - REGION1_TOPLEFT_ANCHOR_POINT.y); - Point region1_pointB = new Point( - REGION1_TOPLEFT_ANCHOR_POINT.x + REGION_WIDTH, - REGION1_TOPLEFT_ANCHOR_POINT.y + REGION_HEIGHT); - Point region2_pointA = new Point( - REGION2_TOPLEFT_ANCHOR_POINT.x, - REGION2_TOPLEFT_ANCHOR_POINT.y); - Point region2_pointB = new Point( - REGION2_TOPLEFT_ANCHOR_POINT.x + REGION_WIDTH, - REGION2_TOPLEFT_ANCHOR_POINT.y + REGION_HEIGHT); - Point region3_pointA = new Point( - REGION3_TOPLEFT_ANCHOR_POINT.x, - REGION3_TOPLEFT_ANCHOR_POINT.y); - Point region3_pointB = new Point( - REGION3_TOPLEFT_ANCHOR_POINT.x + REGION_WIDTH, - REGION3_TOPLEFT_ANCHOR_POINT.y + REGION_HEIGHT); - - /* - * Working variables - */ - Mat region1_Cb, region2_Cb, region3_Cb; - Mat YCrCb = new Mat(); - Mat Cb = new Mat(); - int avg1, avg2, avg3; - - // Volatile since accessed by OpMode thread w/o synchronization - private volatile SkystonePosition position = SkystonePosition.LEFT; - - private Telemetry telemetry; - - public SkystoneDeterminationPipeline(Telemetry telemetry) { - this.telemetry = telemetry; - } - - /* - * This function takes the RGB frame, converts to YCrCb, - * and extracts the Cb channel to the 'Cb' variable - */ - void inputToCb(Mat input) - { - Imgproc.cvtColor(input, YCrCb, Imgproc.COLOR_RGB2YCrCb); - Core.extractChannel(YCrCb, Cb, 2); - } - - @Override - public void init(Mat firstFrame) - { - /* - * We need to call this in order to make sure the 'Cb' - * object is initialized, so that the submats we make - * will still be linked to it on subsequent frames. (If - * the object were to only be initialized in processFrame, - * then the submats would become delinked because the backing - * buffer would be re-allocated the first time a real frame - * was crunched) - */ - inputToCb(firstFrame); - - /* - * Submats are a persistent reference to a region of the parent - * buffer. Any changes to the child affect the parent, and the - * reverse also holds true. - */ - region1_Cb = Cb.submat(new Rect(region1_pointA, region1_pointB)); - region2_Cb = Cb.submat(new Rect(region2_pointA, region2_pointB)); - region3_Cb = Cb.submat(new Rect(region3_pointA, region3_pointB)); - } - - @Override - public Mat processFrame(Mat input) - { - /* - * Overview of what we're doing: - * - * We first convert to YCrCb color space, from RGB color space. - * Why do we do this? Well, in the RGB color space, chroma and - * luma are intertwined. In YCrCb, chroma and luma are separated. - * YCrCb is a 3-channel color space, just like RGB. YCrCb's 3 channels - * are Y, the luma channel (which essentially just a B&W image), the - * Cr channel, which records the difference from red, and the Cb channel, - * which records the difference from blue. Because chroma and luma are - * not related in YCrCb, vision code written to look for certain values - * in the Cr/Cb channels will not be severely affected by differing - * light intensity, since that difference would most likely just be - * reflected in the Y channel. - * - * After we've converted to YCrCb, we extract just the 2nd channel, the - * Cb channel. We do this because stones are bright yellow and contrast - * STRONGLY on the Cb channel against everything else, including SkyStones - * (because SkyStones have a black label). - * - * We then take the average pixel value of 3 different regions on that Cb - * channel, one positioned over each stone. The brightest of the 3 regions - * is where we assume the SkyStone to be, since the normal stones show up - * extremely darkly. - * - * We also draw rectangles on the screen showing where the sample regions - * are, as well as drawing a solid rectangle over top the sample region - * we believe is on top of the SkyStone. - * - * In order for this whole process to work correctly, each sample region - * should be positioned in the center of each of the first 3 stones, and - * be small enough such that only the stone is sampled, and not any of the - * surroundings. - */ - - /* - * Get the Cb channel of the input frame after conversion to YCrCb - */ - inputToCb(input); - - /* - * Compute the average pixel value of each submat region. We're - * taking the average of a single channel buffer, so the value - * we need is at index 0. We could have also taken the average - * pixel value of the 3-channel image, and referenced the value - * at index 2 here. - */ - avg1 = (int) Core.mean(region1_Cb).val[0]; - avg2 = (int) Core.mean(region2_Cb).val[0]; - avg3 = (int) Core.mean(region3_Cb).val[0]; - - /* - * Draw a rectangle showing sample region 1 on the screen. - * Simply a visual aid. Serves no functional purpose. - */ - Imgproc.rectangle( - input, // Buffer to draw on - region1_pointA, // First point which defines the rectangle - region1_pointB, // Second point which defines the rectangle - BLUE, // The color the rectangle is drawn in - 2); // Thickness of the rectangle lines - - /* - * Draw a rectangle showing sample region 2 on the screen. - * Simply a visual aid. Serves no functional purpose. - */ - Imgproc.rectangle( - input, // Buffer to draw on - region2_pointA, // First point which defines the rectangle - region2_pointB, // Second point which defines the rectangle - BLUE, // The color the rectangle is drawn in - 2); // Thickness of the rectangle lines - - /* - * Draw a rectangle showing sample region 3 on the screen. - * Simply a visual aid. Serves no functional purpose. - */ - Imgproc.rectangle( - input, // Buffer to draw on - region3_pointA, // First point which defines the rectangle - region3_pointB, // Second point which defines the rectangle - BLUE, // The color the rectangle is drawn in - 2); // Thickness of the rectangle lines - - - /* - * Find the max of the 3 averages - */ - int maxOneTwo = Math.max(avg1, avg2); - int max = Math.max(maxOneTwo, avg3); - - /* - * Now that we found the max, we actually need to go and - * figure out which sample region that value was from - */ - if(max == avg1) // Was it from region 1? - { - position = SkystonePosition.LEFT; // Record our analysis - - /* - * Draw a solid rectangle on top of the chosen region. - * Simply a visual aid. Serves no functional purpose. - */ - Imgproc.rectangle( - input, // Buffer to draw on - region1_pointA, // First point which defines the rectangle - region1_pointB, // Second point which defines the rectangle - GREEN, // The color the rectangle is drawn in - -1); // Negative thickness means solid fill - } - else if(max == avg2) // Was it from region 2? - { - position = SkystonePosition.CENTER; // Record our analysis - - /* - * Draw a solid rectangle on top of the chosen region. - * Simply a visual aid. Serves no functional purpose. - */ - Imgproc.rectangle( - input, // Buffer to draw on - region2_pointA, // First point which defines the rectangle - region2_pointB, // Second point which defines the rectangle - GREEN, // The color the rectangle is drawn in - -1); // Negative thickness means solid fill - } - else if(max == avg3) // Was it from region 3? - { - position = SkystonePosition.RIGHT; // Record our analysis - - /* - * Draw a solid rectangle on top of the chosen region. - * Simply a visual aid. Serves no functional purpose. - */ - Imgproc.rectangle( - input, // Buffer to draw on - region3_pointA, // First point which defines the rectangle - region3_pointB, // Second point which defines the rectangle - GREEN, // The color the rectangle is drawn in - -1); // Negative thickness means solid fill - } - - telemetry.addData("[Pattern]", position); - telemetry.update(); - - /* - * Render the 'input' buffer to the viewport. But note this is not - * simply rendering the raw camera feed, because we called functions - * to add some annotations to this buffer earlier up. - */ - return input; - } - - /* - * Call this from the OpMode thread to obtain the latest analysis - */ - public SkystonePosition getAnalysis() - { - return position; - } +/* + * Copyright (c) 2020 OpenFTC Team + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.firstinspires.ftc.teamcode; + +import org.firstinspires.ftc.robotcore.external.Telemetry; +import org.opencv.core.Core; +import org.opencv.core.Mat; +import org.opencv.core.Point; +import org.opencv.core.Rect; +import org.opencv.core.Scalar; +import org.opencv.imgproc.Imgproc; +import org.openftc.easyopencv.OpenCvPipeline; + +public class SkystoneDeterminationPipeline extends OpenCvPipeline { + /* + * An enum to define the skystone position + */ + public enum SkystonePosition + { + LEFT, + CENTER, + RIGHT + } + + /* + * Some color constants + */ + public final Scalar BLUE = new Scalar(0, 0, 255); + public final Scalar GREEN = new Scalar(0, 255, 0); + + /* + * The core values which define the location and size of the sample regions + */ + static final Point REGION1_TOPLEFT_ANCHOR_POINT = new Point(109,98); + static final Point REGION2_TOPLEFT_ANCHOR_POINT = new Point(181,98); + static final Point REGION3_TOPLEFT_ANCHOR_POINT = new Point(253,98); + static final int REGION_WIDTH = 20; + static final int REGION_HEIGHT = 20; + + /* + * Points which actually define the sample region rectangles, derived from above values + * + * Example of how points A and B work to define a rectangle + * + * ------------------------------------ + * | (0,0) Point A | + * | | + * | | + * | | + * | | + * | | + * | | + * | Point B (70,50) | + * ------------------------------------ + * + */ + Point region1_pointA = new Point( + REGION1_TOPLEFT_ANCHOR_POINT.x, + REGION1_TOPLEFT_ANCHOR_POINT.y); + Point region1_pointB = new Point( + REGION1_TOPLEFT_ANCHOR_POINT.x + REGION_WIDTH, + REGION1_TOPLEFT_ANCHOR_POINT.y + REGION_HEIGHT); + Point region2_pointA = new Point( + REGION2_TOPLEFT_ANCHOR_POINT.x, + REGION2_TOPLEFT_ANCHOR_POINT.y); + Point region2_pointB = new Point( + REGION2_TOPLEFT_ANCHOR_POINT.x + REGION_WIDTH, + REGION2_TOPLEFT_ANCHOR_POINT.y + REGION_HEIGHT); + Point region3_pointA = new Point( + REGION3_TOPLEFT_ANCHOR_POINT.x, + REGION3_TOPLEFT_ANCHOR_POINT.y); + Point region3_pointB = new Point( + REGION3_TOPLEFT_ANCHOR_POINT.x + REGION_WIDTH, + REGION3_TOPLEFT_ANCHOR_POINT.y + REGION_HEIGHT); + + /* + * Working variables + */ + Mat region1_Cb, region2_Cb, region3_Cb; + Mat YCrCb = new Mat(); + Mat Cb = new Mat(); + int avg1, avg2, avg3; + + // Volatile since accessed by OpMode thread w/o synchronization + private volatile SkystonePosition position = SkystonePosition.LEFT; + + private Telemetry telemetry; + + public SkystoneDeterminationPipeline(Telemetry telemetry) { + this.telemetry = telemetry; + } + + /* + * This function takes the RGB frame, converts to YCrCb, + * and extracts the Cb channel to the 'Cb' variable + */ + void inputToCb(Mat input) + { + Imgproc.cvtColor(input, YCrCb, Imgproc.COLOR_RGB2YCrCb); + Core.extractChannel(YCrCb, Cb, 2); + } + + @Override + public void init(Mat firstFrame) + { + /* + * We need to call this in order to make sure the 'Cb' + * object is initialized, so that the submats we make + * will still be linked to it on subsequent frames. (If + * the object were to only be initialized in processFrame, + * then the submats would become delinked because the backing + * buffer would be re-allocated the first time a real frame + * was crunched) + */ + inputToCb(firstFrame); + + /* + * Submats are a persistent reference to a region of the parent + * buffer. Any changes to the child affect the parent, and the + * reverse also holds true. + */ + region1_Cb = Cb.submat(new Rect(region1_pointA, region1_pointB)); + region2_Cb = Cb.submat(new Rect(region2_pointA, region2_pointB)); + region3_Cb = Cb.submat(new Rect(region3_pointA, region3_pointB)); + } + + @Override + public Mat processFrame(Mat input) + { + /* + * Overview of what we're doing: + * + * We first convert to YCrCb color space, from RGB color space. + * Why do we do this? Well, in the RGB color space, chroma and + * luma are intertwined. In YCrCb, chroma and luma are separated. + * YCrCb is a 3-channel color space, just like RGB. YCrCb's 3 channels + * are Y, the luma channel (which essentially just a B&W image), the + * Cr channel, which records the difference from red, and the Cb channel, + * which records the difference from blue. Because chroma and luma are + * not related in YCrCb, vision code written to look for certain values + * in the Cr/Cb channels will not be severely affected by differing + * light intensity, since that difference would most likely just be + * reflected in the Y channel. + * + * After we've converted to YCrCb, we extract just the 2nd channel, the + * Cb channel. We do this because stones are bright yellow and contrast + * STRONGLY on the Cb channel against everything else, including SkyStones + * (because SkyStones have a black label). + * + * We then take the average pixel value of 3 different regions on that Cb + * channel, one positioned over each stone. The brightest of the 3 regions + * is where we assume the SkyStone to be, since the normal stones show up + * extremely darkly. + * + * We also draw rectangles on the screen showing where the sample regions + * are, as well as drawing a solid rectangle over top the sample region + * we believe is on top of the SkyStone. + * + * In order for this whole process to work correctly, each sample region + * should be positioned in the center of each of the first 3 stones, and + * be small enough such that only the stone is sampled, and not any of the + * surroundings. + */ + + /* + * Get the Cb channel of the input frame after conversion to YCrCb + */ + inputToCb(input); + + /* + * Compute the average pixel value of each submat region. We're + * taking the average of a single channel buffer, so the value + * we need is at index 0. We could have also taken the average + * pixel value of the 3-channel image, and referenced the value + * at index 2 here. + */ + avg1 = (int) Core.mean(region1_Cb).val[0]; + avg2 = (int) Core.mean(region2_Cb).val[0]; + avg3 = (int) Core.mean(region3_Cb).val[0]; + + /* + * Draw a rectangle showing sample region 1 on the screen. + * Simply a visual aid. Serves no functional purpose. + */ + Imgproc.rectangle( + input, // Buffer to draw on + region1_pointA, // First point which defines the rectangle + region1_pointB, // Second point which defines the rectangle + BLUE, // The color the rectangle is drawn in + 2); // Thickness of the rectangle lines + + /* + * Draw a rectangle showing sample region 2 on the screen. + * Simply a visual aid. Serves no functional purpose. + */ + Imgproc.rectangle( + input, // Buffer to draw on + region2_pointA, // First point which defines the rectangle + region2_pointB, // Second point which defines the rectangle + BLUE, // The color the rectangle is drawn in + 2); // Thickness of the rectangle lines + + /* + * Draw a rectangle showing sample region 3 on the screen. + * Simply a visual aid. Serves no functional purpose. + */ + Imgproc.rectangle( + input, // Buffer to draw on + region3_pointA, // First point which defines the rectangle + region3_pointB, // Second point which defines the rectangle + BLUE, // The color the rectangle is drawn in + 2); // Thickness of the rectangle lines + + + /* + * Find the max of the 3 averages + */ + int maxOneTwo = Math.max(avg1, avg2); + int max = Math.max(maxOneTwo, avg3); + + /* + * Now that we found the max, we actually need to go and + * figure out which sample region that value was from + */ + if(max == avg1) // Was it from region 1? + { + position = SkystonePosition.LEFT; // Record our analysis + + /* + * Draw a solid rectangle on top of the chosen region. + * Simply a visual aid. Serves no functional purpose. + */ + Imgproc.rectangle( + input, // Buffer to draw on + region1_pointA, // First point which defines the rectangle + region1_pointB, // Second point which defines the rectangle + GREEN, // The color the rectangle is drawn in + -1); // Negative thickness means solid fill + } + else if(max == avg2) // Was it from region 2? + { + position = SkystonePosition.CENTER; // Record our analysis + + /* + * Draw a solid rectangle on top of the chosen region. + * Simply a visual aid. Serves no functional purpose. + */ + Imgproc.rectangle( + input, // Buffer to draw on + region2_pointA, // First point which defines the rectangle + region2_pointB, // Second point which defines the rectangle + GREEN, // The color the rectangle is drawn in + -1); // Negative thickness means solid fill + } + else if(max == avg3) // Was it from region 3? + { + position = SkystonePosition.RIGHT; // Record our analysis + + /* + * Draw a solid rectangle on top of the chosen region. + * Simply a visual aid. Serves no functional purpose. + */ + Imgproc.rectangle( + input, // Buffer to draw on + region3_pointA, // First point which defines the rectangle + region3_pointB, // Second point which defines the rectangle + GREEN, // The color the rectangle is drawn in + -1); // Negative thickness means solid fill + } + + telemetry.addData("[Pattern]", position); + telemetry.update(); + + /* + * Render the 'input' buffer to the viewport. But note this is not + * simply rendering the raw camera feed, because we called functions + * to add some annotations to this buffer earlier up. + */ + return input; + } + + /* + * Call this from the OpMode thread to obtain the latest analysis + */ + public SkystonePosition getAnalysis() + { + return position; + } } \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/StageSwitchingPipeline.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/StageSwitchingPipeline.java index d1af40b5..123f8f73 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/StageSwitchingPipeline.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/StageSwitchingPipeline.java @@ -1,134 +1,134 @@ -/* - * Copyright (c) 2020 OpenFTC Team - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.firstinspires.ftc.teamcode; - -import org.firstinspires.ftc.robotcore.external.Telemetry; -import org.opencv.core.Core; -import org.opencv.core.Mat; -import org.opencv.core.MatOfPoint; -import org.opencv.core.Scalar; -import org.opencv.imgproc.Imgproc; -import org.openftc.easyopencv.OpenCvPipeline; - -import java.util.ArrayList; -import java.util.List; - -public class StageSwitchingPipeline extends OpenCvPipeline -{ - Mat yCbCrChan2Mat = new Mat(); - Mat thresholdMat = new Mat(); - Mat contoursOnFrameMat = new Mat(); - List contoursList = new ArrayList<>(); - int numContoursFound; - - enum Stage - { - YCbCr_CHAN2, - THRESHOLD, - CONTOURS_OVERLAYED_ON_FRAME, - RAW_IMAGE, - } - - private Stage stageToRenderToViewport = Stage.YCbCr_CHAN2; - private Stage[] stages = Stage.values(); - - private Telemetry telemetry; - - public StageSwitchingPipeline(Telemetry telemetry) { - this.telemetry = telemetry; - } - - @Override - public void onViewportTapped() - { - /* - * Note that this method is invoked from the UI thread - * so whatever we do here, we must do quickly. - */ - - int currentStageNum = stageToRenderToViewport.ordinal(); - - int nextStageNum = currentStageNum + 1; - - if(nextStageNum >= stages.length) - { - nextStageNum = 0; - } - - stageToRenderToViewport = stages[nextStageNum]; - } - - @Override - public Mat processFrame(Mat input) - { - contoursList.clear(); - - /* - * This pipeline finds the contours of yellow blobs such as the Gold Mineral - * from the Rover Ruckus game. - */ - Imgproc.cvtColor(input, yCbCrChan2Mat, Imgproc.COLOR_RGB2YCrCb); - Core.extractChannel(yCbCrChan2Mat, yCbCrChan2Mat, 2); - Imgproc.threshold(yCbCrChan2Mat, thresholdMat, 102, 255, Imgproc.THRESH_BINARY_INV); - Imgproc.findContours(thresholdMat, contoursList, new Mat(), Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE); - numContoursFound = contoursList.size(); - input.copyTo(contoursOnFrameMat); - Imgproc.drawContours(contoursOnFrameMat, contoursList, -1, new Scalar(0, 0, 255), 3, 8); - - telemetry.addData("[Stage]", stageToRenderToViewport); - telemetry.addData("[Found Contours]", "%d", numContoursFound); - telemetry.update(); - - switch (stageToRenderToViewport) - { - case YCbCr_CHAN2: - { - return yCbCrChan2Mat; - } - - case THRESHOLD: - { - return thresholdMat; - } - - case CONTOURS_OVERLAYED_ON_FRAME: - { - return contoursOnFrameMat; - } - - case RAW_IMAGE: - { - return input; - } - - default: - { - return input; - } - } - } - - public int getNumContoursFound() - { - return numContoursFound; - } +/* + * Copyright (c) 2020 OpenFTC Team + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.firstinspires.ftc.teamcode; + +import org.firstinspires.ftc.robotcore.external.Telemetry; +import org.opencv.core.Core; +import org.opencv.core.Mat; +import org.opencv.core.MatOfPoint; +import org.opencv.core.Scalar; +import org.opencv.imgproc.Imgproc; +import org.openftc.easyopencv.OpenCvPipeline; + +import java.util.ArrayList; +import java.util.List; + +public class StageSwitchingPipeline extends OpenCvPipeline +{ + Mat yCbCrChan2Mat = new Mat(); + Mat thresholdMat = new Mat(); + Mat contoursOnFrameMat = new Mat(); + List contoursList = new ArrayList<>(); + int numContoursFound; + + enum Stage + { + YCbCr_CHAN2, + THRESHOLD, + CONTOURS_OVERLAYED_ON_FRAME, + RAW_IMAGE, + } + + private Stage stageToRenderToViewport = Stage.YCbCr_CHAN2; + private Stage[] stages = Stage.values(); + + private Telemetry telemetry; + + public StageSwitchingPipeline(Telemetry telemetry) { + this.telemetry = telemetry; + } + + @Override + public void onViewportTapped() + { + /* + * Note that this method is invoked from the UI thread + * so whatever we do here, we must do quickly. + */ + + int currentStageNum = stageToRenderToViewport.ordinal(); + + int nextStageNum = currentStageNum + 1; + + if(nextStageNum >= stages.length) + { + nextStageNum = 0; + } + + stageToRenderToViewport = stages[nextStageNum]; + } + + @Override + public Mat processFrame(Mat input) + { + contoursList.clear(); + + /* + * This pipeline finds the contours of yellow blobs such as the Gold Mineral + * from the Rover Ruckus game. + */ + Imgproc.cvtColor(input, yCbCrChan2Mat, Imgproc.COLOR_RGB2YCrCb); + Core.extractChannel(yCbCrChan2Mat, yCbCrChan2Mat, 2); + Imgproc.threshold(yCbCrChan2Mat, thresholdMat, 102, 255, Imgproc.THRESH_BINARY_INV); + Imgproc.findContours(thresholdMat, contoursList, new Mat(), Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE); + numContoursFound = contoursList.size(); + input.copyTo(contoursOnFrameMat); + Imgproc.drawContours(contoursOnFrameMat, contoursList, -1, new Scalar(0, 0, 255), 3, 8); + + telemetry.addData("[Stage]", stageToRenderToViewport); + telemetry.addData("[Found Contours]", "%d", numContoursFound); + telemetry.update(); + + switch (stageToRenderToViewport) + { + case YCbCr_CHAN2: + { + return yCbCrChan2Mat; + } + + case THRESHOLD: + { + return thresholdMat; + } + + case CONTOURS_OVERLAYED_ON_FRAME: + { + return contoursOnFrameMat; + } + + case RAW_IMAGE: + { + return input; + } + + default: + { + return input; + } + } + } + + public int getNumContoursFound() + { + return numContoursFound; + } } \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/StoneOrientationAnalysisPipeline.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/StoneOrientationAnalysisPipeline.java index ef22cb90..e76cb50b 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/StoneOrientationAnalysisPipeline.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/StoneOrientationAnalysisPipeline.java @@ -1,467 +1,467 @@ -/* - * Copyright (c) 2020 OpenFTC Team - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.firstinspires.ftc.teamcode; - -import org.openftc.easyopencv.OpenCvPipeline; - -import org.opencv.core.Core; -import org.opencv.core.Mat; -import org.opencv.core.MatOfInt; -import org.opencv.core.MatOfPoint; -import org.opencv.core.MatOfPoint2f; -import org.opencv.core.Point; -import org.opencv.core.RotatedRect; -import org.opencv.core.Scalar; -import org.opencv.core.Size; -import org.opencv.imgproc.Imgproc; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -public class StoneOrientationAnalysisPipeline extends OpenCvPipeline -{ - /* - * Our working image buffers - */ - Mat cbMat = new Mat(); - Mat thresholdMat = new Mat(); - Mat morphedThreshold = new Mat(); - Mat contoursOnPlainImageMat = new Mat(); - - /* - * Threshold values - */ - static final int CB_CHAN_MASK_THRESHOLD = 80; - static final double DENSITY_UPRIGHT_THRESHOLD = 0.03; - - /* - * The elements we use for noise reduction - */ - Mat erodeElement = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3)); - Mat dilateElement = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(6, 6)); - - /* - * Colors - */ - static final Scalar TEAL = new Scalar(3, 148, 252); - static final Scalar PURPLE = new Scalar(158, 52, 235); - static final Scalar RED = new Scalar(255, 0, 0); - static final Scalar GREEN = new Scalar(0, 255, 0); - static final Scalar BLUE = new Scalar(0, 0, 255); - - static final int CONTOUR_LINE_THICKNESS = 2; - static final int CB_CHAN_IDX = 2; - - static class AnalyzedStone - { - StoneOrientation orientation; - double angle; - } - - enum StoneOrientation - { - UPRIGHT, - NOT_UPRIGHT - } - - ArrayList internalStoneList = new ArrayList<>(); - volatile ArrayList clientStoneList = new ArrayList<>(); - - /* - * Some stuff to handle returning our various buffers - */ - enum Stage - { - FINAL, - Cb, - MASK, - MASK_NR, - CONTOURS; - } - - Stage[] stages = Stage.values(); - - // Keep track of what stage the viewport is showing - int stageNum = 0; - - @Override - public void onViewportTapped() - { - /* - * Note that this method is invoked from the UI thread - * so whatever we do here, we must do quickly. - */ - - int nextStageNum = stageNum + 1; - - if(nextStageNum >= stages.length) - { - nextStageNum = 0; - } - - stageNum = nextStageNum; - } - - @Override - public Mat processFrame(Mat input) - { - // We'll be updating this with new data below - internalStoneList.clear(); - - /* - * Run the image processing - */ - for(MatOfPoint contour : findContours(input)) - { - analyzeContour(contour, input); - } - - clientStoneList = new ArrayList<>(internalStoneList); - - /* - * Decide which buffer to send to the viewport - */ - switch (stages[stageNum]) - { - case Cb: - { - return cbMat; - } - - case FINAL: - { - return input; - } - - case MASK: - { - return thresholdMat; - } - - case MASK_NR: - { - return morphedThreshold; - } - - case CONTOURS: - { - return contoursOnPlainImageMat; - } - } - - return input; - } - - public ArrayList getDetectedStones() - { - return clientStoneList; - } - - ArrayList findContours(Mat input) - { - // A list we'll be using to store the contours we find - ArrayList contoursList = new ArrayList<>(); - - // Convert the input image to YCrCb color space, then extract the Cb channel - Imgproc.cvtColor(input, cbMat, Imgproc.COLOR_RGB2YCrCb); - Core.extractChannel(cbMat, cbMat, CB_CHAN_IDX); - - // Threshold the Cb channel to form a mask, then run some noise reduction - Imgproc.threshold(cbMat, thresholdMat, CB_CHAN_MASK_THRESHOLD, 255, Imgproc.THRESH_BINARY_INV); - morphMask(thresholdMat, morphedThreshold); - - // Ok, now actually look for the contours! We only look for external contours. - Imgproc.findContours(morphedThreshold, contoursList, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE); - - // We do draw the contours we find, but not to the main input buffer. - input.copyTo(contoursOnPlainImageMat); - Imgproc.drawContours(contoursOnPlainImageMat, contoursList, -1, BLUE, CONTOUR_LINE_THICKNESS, 8); - - return contoursList; - } - - void morphMask(Mat input, Mat output) - { - /* - * Apply some erosion and dilation for noise reduction - */ - - Imgproc.erode(input, output, erodeElement); - Imgproc.erode(output, output, erodeElement); - - Imgproc.dilate(output, output, dilateElement); - Imgproc.dilate(output, output, dilateElement); - } - - void analyzeContour(MatOfPoint contour, Mat input) - { - // Transform the contour to a different format - Point[] points = contour.toArray(); - MatOfPoint2f contour2f = new MatOfPoint2f(contour.toArray()); - - // Do a rect fit to the contour, and draw it on the screen - RotatedRect rotatedRectFitToContour = Imgproc.minAreaRect(contour2f); - drawRotatedRect(rotatedRectFitToContour, input); - - // The angle OpenCV gives us can be ambiguous, so look at the shape of - // the rectangle to fix that. - double rotRectAngle = rotatedRectFitToContour.angle; - if (rotatedRectFitToContour.size.width < rotatedRectFitToContour.size.height) - { - rotRectAngle += 90; - } - - // Figure out the slope of a line which would run through the middle, lengthwise - // (Slope as in m from 'Y = mx + b') - double midlineSlope = Math.tan(Math.toRadians(rotRectAngle)); - - // We're going to split the this contour into two regions: one region for the points - // which fall above the midline, and one region for the points which fall below. - // We'll need a place to store the points as we split them, so we make ArrayLists - ArrayList aboveMidline = new ArrayList<>(points.length/2); - ArrayList belowMidline = new ArrayList<>(points.length/2); - - // Ok, now actually split the contour into those two regions we discussed earlier! - for(Point p : points) - { - if(rotatedRectFitToContour.center.y - p.y > midlineSlope * (rotatedRectFitToContour.center.x - p.x)) - { - aboveMidline.add(p); - } - else - { - belowMidline.add(p); - } - } - - // Now that we've split the contour into those two regions, we analyze each - // region independently. - ContourRegionAnalysis aboveMidlineMetrics = analyzeContourRegion(aboveMidline); - ContourRegionAnalysis belowMidlineMetrics = analyzeContourRegion(belowMidline); - - if(aboveMidlineMetrics == null || belowMidlineMetrics == null) - { - return; // Get out of dodge - } - - // We're going to draw line from the center of the bounding rect, to outside the bounding rect, in the - // direction of the side of the stone with the nubs. - Point displOfOrientationLinePoint2 = computeDisplacementForSecondPointOfStoneOrientationLine(rotatedRectFitToContour, rotRectAngle); - - /* - * If the difference in the densities of the two regions exceeds the threshold, - * then we assume the stone is on its side. Otherwise, if the difference is inside - * of the threshold, we assume it's upright. - */ - if(aboveMidlineMetrics.density < belowMidlineMetrics.density - DENSITY_UPRIGHT_THRESHOLD) - { - /* - * Assume the stone is on its side, with the top contour region being the - * one which contains the nubs - */ - - // Draw that line we were just talking about - Imgproc.line( - input, // Buffer we're drawing on - new Point( // First point of the line (center of bounding rect) - rotatedRectFitToContour.center.x, - rotatedRectFitToContour.center.y), - new Point( // Second point of the line (center - displacement we calculated earlier) - rotatedRectFitToContour.center.x-displOfOrientationLinePoint2.x, - rotatedRectFitToContour.center.y-displOfOrientationLinePoint2.y), - PURPLE, // Color we're drawing the line in - 2); // Thickness of the line we're drawing - - // We outline the contour region that we assumed to be the side with the nubs - Imgproc.drawContours(input, aboveMidlineMetrics.listHolderOfMatOfPoint, -1, TEAL, 2, 8); - - // Compute the absolute angle of the stone - double angle = -(rotRectAngle-90); - - // "Tag" the stone with text stating its absolute angle - drawTagText(rotatedRectFitToContour, Integer.toString((int) Math.round(angle))+" deg", input); - - AnalyzedStone analyzedStone = new AnalyzedStone(); - analyzedStone.angle = angle; - analyzedStone.orientation = StoneOrientation.NOT_UPRIGHT; - internalStoneList.add(analyzedStone); - } - else if(belowMidlineMetrics.density < aboveMidlineMetrics.density - DENSITY_UPRIGHT_THRESHOLD) - { - /* - * Assume the stone is on its side, with the bottom contour region being the - * one which contains the nubs - */ - - // Draw that line we were just talking about - Imgproc.line( - input, // Buffer we're drawing on - new Point( // First point of the line (center + displacement we calculated earlier) - rotatedRectFitToContour.center.x+displOfOrientationLinePoint2.x, - rotatedRectFitToContour.center.y+displOfOrientationLinePoint2.y), - new Point( // Second point of the line (center of bounding rect) - rotatedRectFitToContour.center.x, - rotatedRectFitToContour.center.y), - PURPLE, // Color we're drawing the line in - 2); // Thickness of the line we're drawing - - // We outline the contour region that we assumed to be the side with the nubs - Imgproc.drawContours(input, belowMidlineMetrics.listHolderOfMatOfPoint, -1, TEAL, 2, 8); - - // Compute the absolute angle of the stone - double angle = -(rotRectAngle-270); - - // "Tag" the stone with text stating its absolute angle - drawTagText(rotatedRectFitToContour, Integer.toString((int) Math.round(angle))+" deg", input); - - AnalyzedStone analyzedStone = new AnalyzedStone(); - analyzedStone.angle = angle; - analyzedStone.orientation = StoneOrientation.NOT_UPRIGHT; - internalStoneList.add(analyzedStone); - } - else - { - /* - * Assume the stone is upright - */ - - drawTagText(rotatedRectFitToContour, "UPRIGHT", input); - - AnalyzedStone analyzedStone = new AnalyzedStone(); - analyzedStone.angle = rotRectAngle; - analyzedStone.orientation = StoneOrientation.UPRIGHT; - internalStoneList.add(analyzedStone); - } - } - - static class ContourRegionAnalysis - { - /* - * This class holds the results of analyzeContourRegion() - */ - - double hullArea; - double contourArea; - double density; - List listHolderOfMatOfPoint; - } - - static ContourRegionAnalysis analyzeContourRegion(ArrayList contourPoints) - { - // drawContours() requires a LIST of contours (there's no singular drawContour() - // method), so we have to make a list, even though we're only going to use a single - // position in it... - MatOfPoint matOfPoint = new MatOfPoint(); - matOfPoint.fromList(contourPoints); - List listHolderOfMatOfPoint = Arrays.asList(matOfPoint); - - // Compute the convex hull of the contour - MatOfInt hullMatOfInt = new MatOfInt(); - Imgproc.convexHull(matOfPoint, hullMatOfInt); - - // Was the convex hull calculation successful? - if(hullMatOfInt.toArray().length > 0) - { - // The convex hull calculation tells us the INDEX of the points which - // which were passed in eariler which form the convex hull. That's all - // well and good, but now we need filter out that original list to find - // the actual POINTS which form the convex hull - Point[] hullPoints = new Point[hullMatOfInt.rows()]; - List hullContourIdxList = hullMatOfInt.toList(); - - for (int i = 0; i < hullContourIdxList.size(); i++) - { - hullPoints[i] = contourPoints.get(hullContourIdxList.get(i)); - } - - ContourRegionAnalysis analysis = new ContourRegionAnalysis(); - analysis.listHolderOfMatOfPoint = listHolderOfMatOfPoint; - - // Compute the hull area - analysis.hullArea = Imgproc.contourArea(new MatOfPoint(hullPoints)); - - // Compute the original contour area - analysis.contourArea = Imgproc.contourArea(listHolderOfMatOfPoint.get(0)); - - // Compute the contour density. This is the ratio of the contour area to the - // area of the convex hull formed by the contour - analysis.density = analysis.contourArea / analysis.hullArea; - - return analysis; - } - else - { - return null; - } - } - - static Point computeDisplacementForSecondPointOfStoneOrientationLine(RotatedRect rect, double unambiguousAngle) - { - // Note: we return a point, but really it's not a point in space, we're - // simply using it to hold X & Y displacement values from the middle point - // of the bounding rect. - Point point = new Point(); - - // Figure out the length of the short side of the rect - double shortSideLen = Math.min(rect.size.width, rect.size.height); - - // We draw a line that's 3/4 of the length of the short side of the rect - double lineLength = shortSideLen * .75; - - // The line is to be drawn at 90 deg relative to the midline running through - // the rect lengthwise - point.x = (int) (lineLength * Math.cos(Math.toRadians(unambiguousAngle+90))); - point.y = (int) (lineLength * Math.sin(Math.toRadians(unambiguousAngle+90))); - - return point; - } - - static void drawTagText(RotatedRect rect, String text, Mat mat) - { - Imgproc.putText( - mat, // The buffer we're drawing on - text, // The text we're drawing - new Point( // The anchor point for the text - rect.center.x-50, // x anchor point - rect.center.y+25), // y anchor point - Imgproc.FONT_HERSHEY_PLAIN, // Font - 1, // Font size - TEAL, // Font color - 1); // Font thickness - } - - static void drawRotatedRect(RotatedRect rect, Mat drawOn) - { - /* - * Draws a rotated rect by drawing each of the 4 lines individually - */ - - Point[] points = new Point[4]; - rect.points(points); - - for(int i = 0; i < 4; ++i) - { - Imgproc.line(drawOn, points[i], points[(i+1)%4], RED, 2); - } - } +/* + * Copyright (c) 2020 OpenFTC Team + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.firstinspires.ftc.teamcode; + +import org.openftc.easyopencv.OpenCvPipeline; + +import org.opencv.core.Core; +import org.opencv.core.Mat; +import org.opencv.core.MatOfInt; +import org.opencv.core.MatOfPoint; +import org.opencv.core.MatOfPoint2f; +import org.opencv.core.Point; +import org.opencv.core.RotatedRect; +import org.opencv.core.Scalar; +import org.opencv.core.Size; +import org.opencv.imgproc.Imgproc; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class StoneOrientationAnalysisPipeline extends OpenCvPipeline +{ + /* + * Our working image buffers + */ + Mat cbMat = new Mat(); + Mat thresholdMat = new Mat(); + Mat morphedThreshold = new Mat(); + Mat contoursOnPlainImageMat = new Mat(); + + /* + * Threshold values + */ + static final int CB_CHAN_MASK_THRESHOLD = 80; + static final double DENSITY_UPRIGHT_THRESHOLD = 0.03; + + /* + * The elements we use for noise reduction + */ + Mat erodeElement = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3)); + Mat dilateElement = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(6, 6)); + + /* + * Colors + */ + static final Scalar TEAL = new Scalar(3, 148, 252); + static final Scalar PURPLE = new Scalar(158, 52, 235); + static final Scalar RED = new Scalar(255, 0, 0); + static final Scalar GREEN = new Scalar(0, 255, 0); + static final Scalar BLUE = new Scalar(0, 0, 255); + + static final int CONTOUR_LINE_THICKNESS = 2; + static final int CB_CHAN_IDX = 2; + + static class AnalyzedStone + { + StoneOrientation orientation; + double angle; + } + + enum StoneOrientation + { + UPRIGHT, + NOT_UPRIGHT + } + + ArrayList internalStoneList = new ArrayList<>(); + volatile ArrayList clientStoneList = new ArrayList<>(); + + /* + * Some stuff to handle returning our various buffers + */ + enum Stage + { + FINAL, + Cb, + MASK, + MASK_NR, + CONTOURS; + } + + Stage[] stages = Stage.values(); + + // Keep track of what stage the viewport is showing + int stageNum = 0; + + @Override + public void onViewportTapped() + { + /* + * Note that this method is invoked from the UI thread + * so whatever we do here, we must do quickly. + */ + + int nextStageNum = stageNum + 1; + + if(nextStageNum >= stages.length) + { + nextStageNum = 0; + } + + stageNum = nextStageNum; + } + + @Override + public Mat processFrame(Mat input) + { + // We'll be updating this with new data below + internalStoneList.clear(); + + /* + * Run the image processing + */ + for(MatOfPoint contour : findContours(input)) + { + analyzeContour(contour, input); + } + + clientStoneList = new ArrayList<>(internalStoneList); + + /* + * Decide which buffer to send to the viewport + */ + switch (stages[stageNum]) + { + case Cb: + { + return cbMat; + } + + case FINAL: + { + return input; + } + + case MASK: + { + return thresholdMat; + } + + case MASK_NR: + { + return morphedThreshold; + } + + case CONTOURS: + { + return contoursOnPlainImageMat; + } + } + + return input; + } + + public ArrayList getDetectedStones() + { + return clientStoneList; + } + + ArrayList findContours(Mat input) + { + // A list we'll be using to store the contours we find + ArrayList contoursList = new ArrayList<>(); + + // Convert the input image to YCrCb color space, then extract the Cb channel + Imgproc.cvtColor(input, cbMat, Imgproc.COLOR_RGB2YCrCb); + Core.extractChannel(cbMat, cbMat, CB_CHAN_IDX); + + // Threshold the Cb channel to form a mask, then run some noise reduction + Imgproc.threshold(cbMat, thresholdMat, CB_CHAN_MASK_THRESHOLD, 255, Imgproc.THRESH_BINARY_INV); + morphMask(thresholdMat, morphedThreshold); + + // Ok, now actually look for the contours! We only look for external contours. + Imgproc.findContours(morphedThreshold, contoursList, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE); + + // We do draw the contours we find, but not to the main input buffer. + input.copyTo(contoursOnPlainImageMat); + Imgproc.drawContours(contoursOnPlainImageMat, contoursList, -1, BLUE, CONTOUR_LINE_THICKNESS, 8); + + return contoursList; + } + + void morphMask(Mat input, Mat output) + { + /* + * Apply some erosion and dilation for noise reduction + */ + + Imgproc.erode(input, output, erodeElement); + Imgproc.erode(output, output, erodeElement); + + Imgproc.dilate(output, output, dilateElement); + Imgproc.dilate(output, output, dilateElement); + } + + void analyzeContour(MatOfPoint contour, Mat input) + { + // Transform the contour to a different format + Point[] points = contour.toArray(); + MatOfPoint2f contour2f = new MatOfPoint2f(contour.toArray()); + + // Do a rect fit to the contour, and draw it on the screen + RotatedRect rotatedRectFitToContour = Imgproc.minAreaRect(contour2f); + drawRotatedRect(rotatedRectFitToContour, input); + + // The angle OpenCV gives us can be ambiguous, so look at the shape of + // the rectangle to fix that. + double rotRectAngle = rotatedRectFitToContour.angle; + if (rotatedRectFitToContour.size.width < rotatedRectFitToContour.size.height) + { + rotRectAngle += 90; + } + + // Figure out the slope of a line which would run through the middle, lengthwise + // (Slope as in m from 'Y = mx + b') + double midlineSlope = Math.tan(Math.toRadians(rotRectAngle)); + + // We're going to split the this contour into two regions: one region for the points + // which fall above the midline, and one region for the points which fall below. + // We'll need a place to store the points as we split them, so we make ArrayLists + ArrayList aboveMidline = new ArrayList<>(points.length/2); + ArrayList belowMidline = new ArrayList<>(points.length/2); + + // Ok, now actually split the contour into those two regions we discussed earlier! + for(Point p : points) + { + if(rotatedRectFitToContour.center.y - p.y > midlineSlope * (rotatedRectFitToContour.center.x - p.x)) + { + aboveMidline.add(p); + } + else + { + belowMidline.add(p); + } + } + + // Now that we've split the contour into those two regions, we analyze each + // region independently. + ContourRegionAnalysis aboveMidlineMetrics = analyzeContourRegion(aboveMidline); + ContourRegionAnalysis belowMidlineMetrics = analyzeContourRegion(belowMidline); + + if(aboveMidlineMetrics == null || belowMidlineMetrics == null) + { + return; // Get out of dodge + } + + // We're going to draw line from the center of the bounding rect, to outside the bounding rect, in the + // direction of the side of the stone with the nubs. + Point displOfOrientationLinePoint2 = computeDisplacementForSecondPointOfStoneOrientationLine(rotatedRectFitToContour, rotRectAngle); + + /* + * If the difference in the densities of the two regions exceeds the threshold, + * then we assume the stone is on its side. Otherwise, if the difference is inside + * of the threshold, we assume it's upright. + */ + if(aboveMidlineMetrics.density < belowMidlineMetrics.density - DENSITY_UPRIGHT_THRESHOLD) + { + /* + * Assume the stone is on its side, with the top contour region being the + * one which contains the nubs + */ + + // Draw that line we were just talking about + Imgproc.line( + input, // Buffer we're drawing on + new Point( // First point of the line (center of bounding rect) + rotatedRectFitToContour.center.x, + rotatedRectFitToContour.center.y), + new Point( // Second point of the line (center - displacement we calculated earlier) + rotatedRectFitToContour.center.x-displOfOrientationLinePoint2.x, + rotatedRectFitToContour.center.y-displOfOrientationLinePoint2.y), + PURPLE, // Color we're drawing the line in + 2); // Thickness of the line we're drawing + + // We outline the contour region that we assumed to be the side with the nubs + Imgproc.drawContours(input, aboveMidlineMetrics.listHolderOfMatOfPoint, -1, TEAL, 2, 8); + + // Compute the absolute angle of the stone + double angle = -(rotRectAngle-90); + + // "Tag" the stone with text stating its absolute angle + drawTagText(rotatedRectFitToContour, Integer.toString((int) Math.round(angle))+" deg", input); + + AnalyzedStone analyzedStone = new AnalyzedStone(); + analyzedStone.angle = angle; + analyzedStone.orientation = StoneOrientation.NOT_UPRIGHT; + internalStoneList.add(analyzedStone); + } + else if(belowMidlineMetrics.density < aboveMidlineMetrics.density - DENSITY_UPRIGHT_THRESHOLD) + { + /* + * Assume the stone is on its side, with the bottom contour region being the + * one which contains the nubs + */ + + // Draw that line we were just talking about + Imgproc.line( + input, // Buffer we're drawing on + new Point( // First point of the line (center + displacement we calculated earlier) + rotatedRectFitToContour.center.x+displOfOrientationLinePoint2.x, + rotatedRectFitToContour.center.y+displOfOrientationLinePoint2.y), + new Point( // Second point of the line (center of bounding rect) + rotatedRectFitToContour.center.x, + rotatedRectFitToContour.center.y), + PURPLE, // Color we're drawing the line in + 2); // Thickness of the line we're drawing + + // We outline the contour region that we assumed to be the side with the nubs + Imgproc.drawContours(input, belowMidlineMetrics.listHolderOfMatOfPoint, -1, TEAL, 2, 8); + + // Compute the absolute angle of the stone + double angle = -(rotRectAngle-270); + + // "Tag" the stone with text stating its absolute angle + drawTagText(rotatedRectFitToContour, Integer.toString((int) Math.round(angle))+" deg", input); + + AnalyzedStone analyzedStone = new AnalyzedStone(); + analyzedStone.angle = angle; + analyzedStone.orientation = StoneOrientation.NOT_UPRIGHT; + internalStoneList.add(analyzedStone); + } + else + { + /* + * Assume the stone is upright + */ + + drawTagText(rotatedRectFitToContour, "UPRIGHT", input); + + AnalyzedStone analyzedStone = new AnalyzedStone(); + analyzedStone.angle = rotRectAngle; + analyzedStone.orientation = StoneOrientation.UPRIGHT; + internalStoneList.add(analyzedStone); + } + } + + static class ContourRegionAnalysis + { + /* + * This class holds the results of analyzeContourRegion() + */ + + double hullArea; + double contourArea; + double density; + List listHolderOfMatOfPoint; + } + + static ContourRegionAnalysis analyzeContourRegion(ArrayList contourPoints) + { + // drawContours() requires a LIST of contours (there's no singular drawContour() + // method), so we have to make a list, even though we're only going to use a single + // position in it... + MatOfPoint matOfPoint = new MatOfPoint(); + matOfPoint.fromList(contourPoints); + List listHolderOfMatOfPoint = Arrays.asList(matOfPoint); + + // Compute the convex hull of the contour + MatOfInt hullMatOfInt = new MatOfInt(); + Imgproc.convexHull(matOfPoint, hullMatOfInt); + + // Was the convex hull calculation successful? + if(hullMatOfInt.toArray().length > 0) + { + // The convex hull calculation tells us the INDEX of the points which + // which were passed in eariler which form the convex hull. That's all + // well and good, but now we need filter out that original list to find + // the actual POINTS which form the convex hull + Point[] hullPoints = new Point[hullMatOfInt.rows()]; + List hullContourIdxList = hullMatOfInt.toList(); + + for (int i = 0; i < hullContourIdxList.size(); i++) + { + hullPoints[i] = contourPoints.get(hullContourIdxList.get(i)); + } + + ContourRegionAnalysis analysis = new ContourRegionAnalysis(); + analysis.listHolderOfMatOfPoint = listHolderOfMatOfPoint; + + // Compute the hull area + analysis.hullArea = Imgproc.contourArea(new MatOfPoint(hullPoints)); + + // Compute the original contour area + analysis.contourArea = Imgproc.contourArea(listHolderOfMatOfPoint.get(0)); + + // Compute the contour density. This is the ratio of the contour area to the + // area of the convex hull formed by the contour + analysis.density = analysis.contourArea / analysis.hullArea; + + return analysis; + } + else + { + return null; + } + } + + static Point computeDisplacementForSecondPointOfStoneOrientationLine(RotatedRect rect, double unambiguousAngle) + { + // Note: we return a point, but really it's not a point in space, we're + // simply using it to hold X & Y displacement values from the middle point + // of the bounding rect. + Point point = new Point(); + + // Figure out the length of the short side of the rect + double shortSideLen = Math.min(rect.size.width, rect.size.height); + + // We draw a line that's 3/4 of the length of the short side of the rect + double lineLength = shortSideLen * .75; + + // The line is to be drawn at 90 deg relative to the midline running through + // the rect lengthwise + point.x = (int) (lineLength * Math.cos(Math.toRadians(unambiguousAngle+90))); + point.y = (int) (lineLength * Math.sin(Math.toRadians(unambiguousAngle+90))); + + return point; + } + + static void drawTagText(RotatedRect rect, String text, Mat mat) + { + Imgproc.putText( + mat, // The buffer we're drawing on + text, // The text we're drawing + new Point( // The anchor point for the text + rect.center.x-50, // x anchor point + rect.center.y+25), // y anchor point + Imgproc.FONT_HERSHEY_PLAIN, // Font + 1, // Font size + TEAL, // Font color + 1); // Font thickness + } + + static void drawRotatedRect(RotatedRect rect, Mat drawOn) + { + /* + * Draws a rotated rect by drawing each of the 4 lines individually + */ + + Point[] points = new Point[4]; + rect.points(points); + + for(int i = 0; i < 4; ++i) + { + Imgproc.line(drawOn, points[i], points[(i+1)%4], RED, 2); + } + } } \ No newline at end of file diff --git a/USAGE.md b/USAGE.md index 8ed26315..b1f54d67 100644 --- a/USAGE.md +++ b/USAGE.md @@ -1,420 +1,420 @@ -# Usage Explanation - -### This guide is still a work in progress - -## Welcome! - -Thank you for your interest in EOCV-Sim :) - -We made this tool in hopes that it will be useful for all FTC teams seeking a way of learning and developing their seasonal OpenCV algorithms in a easy and straightforward way, while also providing some extra tools to improve the experience of developing such algorithms. - -The main purpose of this software is to simulate the package & class structure of OpenFTC's EasyOpenCV and a little bit of the FTC SDK, while also providing OpenCV functionality and a simple GUI. - -By simulating the aforementioned structure, it allows the imports, class names, etc. to be the same as they would if you were using the FTC SDK with EasyOpenCV, allowing you to simply copy paste your vision code onto your Android Studio project once you want to transfer it to a robot.
- -## Table of Contents -- [Workspaces & VS Code](#workspaces--vs-code) -- [IntelliJ Project Structure (with the old installation method)](#intellij-project-structure-with-the-old-installation-method) - - [Creating pipelines](#creating-pipelines-with-the-old-installation-method) -- [Empty Sample Pipeline](#empty-sample-pipeline) -- [Input Sources](#input-sources) - - [Creating an input source](#creating-an-input-source) -- [Telemetry](#telemetry) -- [Variable Tuner](#variable-tuner) - - [Functionality](#tuner-functionality) - - [Configuration](#tuner-configuration) - - [Sample Usage](#sample-usage-of-the-variable-tuner) - -## Workspaces & VS Code - -**This part is applicable with any installation method** - -### Introduction to workspaces - -Workspaces are the new major feature introduced in v3.0.0! - -A workspace basically consists of a folder containing `.java` source files and resource files, which are compiled on the fly by EOCV-Sim. This removes the need of having to use Gradle for running builds, or even allowing to see code changes in real time within a few seconds or even milliseconds! - -Note that a Java Development Kit (JDK) is needed for using this feature, since Java Runtime Environments (JREs) don't come with a compiler packaged, - -The simulator watches for file changes in the background every 800ms, and triggers a build automatically. This can be greately beneficial for writing pipelines, plus the variable tuner that also allows to change variables in real time to reflect changes immediately. The simulator also tries to snapshot the state of the pipeline selected before the build, and try to apply it to the (possibly new if it was compiled on runtime) selected pipeline if the package and class names of the old and the new pipeline match, therefore values tuned with the variable tuner can be kept between builds even if the source code has changed. - -The simulator creates and selects by default a workspace in the user folder, `~/.eocvsim/default_workspace`, which contains a sample GrayscalePipeline that is compiled and added on runtime, but you can change it by doing the following steps: - -1) Go under the "Pipelines" section, click the "Workspace" and finally "Select workspace". Or alternatively, you can also go to Workspace -> Select workspace - - - -2) Select a folder in the file explorer that pops up - -3) Done! The sim should select the folder as a workspace, create a `eocvsim_workspace.json` file if it doesn't exist, start running a build and watch for file changes in the selected folder - -### VS Code - -### The `eocvsim_workspace.json` file - -## IntelliJ project structure (with the old installation method) - -**This part is only applicable if you downloaded EOCV-Sim with the [old method explained in the README](/README.md#altenative-installation-method-intellij-idea)** - -EOCV-Sim uses Gradle starting from v2.0.0, because of this, the project structure is a bit different. For finding the package in which the pipelines have to be placed:
-1) Pop out the parent EOCV-Sim project folder by clicking on the "*>*" arrow -2) Find the TeamCode module (folder) and pop it out just like before -3) Find the src folder and open it -4) Now you will find the *org.firstinspires.ftc.teamcode* package, in which you should place all your pipelines and some sample pipelines are already there.
- -These steps are illustrated in this gif:
- -
- -### Creating pipelines (with the old installation method) - -**This part is also only applicable if you downloaded EOCV-Sim with the [old method explained in the README](/README.md#altenative-installation-method-intellij-idea)** - -As said before, all of the pipeline classes **should be** placed under the *org.firstinspires.ftc.teamcode* package, in the *TeamCode* module. This way, they will be -automatically detected by the simulator and will be selectionable from the GUI. - -
- -*(Also, the simulator already comes by default with some EasyOpenCV samples)*
- -To create a new java class, follow these steps:
-1) In the project files menu, open the TeamCode module -2) Find the *org.firstinspires.ftc.teamcode* package and right click on it -3) On the context menu, click on *New > Java Class* -4) A new menu will appear, type a name and make sure the *Class* option is selected -5) Once you have typed a name, press enter and the class will be created - -Here's a quick gif illustrating these steps:
- -
- -## Empty sample pipeline - -If you want your class to be a pipeline, it **should also** extend the EOCV's OpenCvPipeline abstract class and override the processFrame() method.

-Here's a empty pipeline template, with the SamplePipeline class we created before: - -```java -package org.firstinspires.ftc.teamcode; - -import org.opencv.core.Mat; -import org.openftc.easyopencv.OpenCvPipeline; - -public class SamplePipeline extends OpenCvPipeline { - - @Override - public void init(Mat input) { - /* Executed once, when the pipeline is selected */ - } - - @Override - public Mat processFrame(Mat input) { - /* Executed each frame, the returned mat will be the one displayed */ - /* Processing and detection stuff here */ - return input; // Return the input mat - // (Or a new, processed mat) - } - - @Override - public void onViewportTapped() { - /* - * Executed everytime when the pipeline view is tapped/clicked. - * This is executed from the UI thread, so whatever you do here, - * it must be done it quickly. - */ - } - -} -``` - -### For more detailed information about pipelines, make sure to check out the [EasyOpenCV docs](https://github.com/OpenFTC/EasyOpenCV/blob/master/doc/user_docs/pipelines_overview.md) - -## Input Sources - -To allow multiple ways to test your pipeline, the simulator comes with *Input Sources*, which are the ones in charge of giving your pipeline the input Mats, As of right now, the sim has three types of Input Sources: - -- **Image Source:** - - These will feed your pipeline with a static image loaded in your computer's hard drive. - - To save resources, your pipeline will just run once when you select an image source, but you can optionally resume the pipeline execution by clicking the "Pause" button under the pipeline selector. -- **Camera Source:** - - These will feed your pipeline with a constantly changing video stream from a specified camera plugged in your computer. - - Unlike the image sources, these will not pause the execution of you pipeline by default, but you can click the "Pause" button to pause it at any time. -- **Video Source:** - - These will feed your pipeline with a constantly changing video stream from a file in your hard drive, pause rules are the same as camera sources. - - Most tested video format is *\*.avi*, although it depends on your operating system's support. - -### Creating an Input Source - -1) From your Operating System's file manager, grab a media file such as an image or a video. - -## Telemetry - -There's also an SDK-like Telemetry implementation in the sim. -In 1.1.0 (when it was introduced) you could simply access it from your pipeline since it was an instance variable ```telemetry```. - -But, starting 2.0.0, to make it more alike to an actual EOCV pipeline, you need to implement a public constructor which takes a Telemetry parameter, then creating and setting an instance variable from that constructor: - -```java -package org.firstinspires.ftc.teamcode; - -import org.opencv.core.Mat; -import org.openftc.easyopencv.OpenCvPipeline; - -import org.firstinspires.ftc.robotcore.external.Telemetry; - -public class TelemetryPipeline extends OpenCvPipeline { - - Telemetry telemetry; - - public TelemetryPipeline(Telemetry telemetry) { - this.telemetry = telemetry; - } - - @Override - public Mat processFrame(Mat input) { - telemetry.addData("[Hello]", "World!"); - telemetry.update(); - return input; // Return the input mat - } - -} -``` - -Which then produces the following result:
- -
- -For further information about telemetry, you can check out the [SDK docs on Telemetry](https://ftctechnh.github.io/ftc_app/doc/javadoc/org/firstinspires/ftc/robotcore/external/Telemetry.html), note that not all the methods are implemented for EOCV-Sim - -## Variable Tuner - -From 2.0.0 and on, there's a variable tuner implemented into the simulator, inspired by the one in FTC Dashboard, it allows to edit public, non-final variables from your pipeline in real time seamlessly through Java reflection.
- -This variable tuner can be found at the bottom part of the sim, click on the divider bar to open it:
- -
-
- -This screenshot is from the DefaultPipeline (the one selected when the simulator opens). This variable controls the blur value for the output Mat. You can play with this value to see the tuner functionality.

-If we look into the DefaultPipeline code, we can see that it is simply a **public int** instance variable, not marked as final (alongside with the Telemetry initialization stuff we explained before):
- -
- -The tuner supports a handful of Java types such as most primivites (int, boolean...) and some other types from OpenCV.
-The full list of types currently supported by the tuner on the latest version is:
- - Java: - - int (or Integer) - - float (or Float) - - double (or Double) - - long (or Long) - - boolean (or Boolean) - - String - - Enums - - OpenCV: - - Scalar - - Rect - - Point - -### Tuner functionality - -In the screenshot above, you might have noticed we have three buttons in the field (these buttons appear on field types with at least one textbox/slider). Those were introduced in 2.1.0 to provide extra functionality to the tuner. We have three buttons (options), five parts: - -
- -1) **Text fields/slider toggle** - - Toggles between sliders and textboxes for setting the value to this field -2) **Config** - - Configures various aspects of this tunable field, such as the slider range and picker's color space -3) **Color picker** - - Turns on "color picker" mode, which allows to grab a single pixel from the image and sets it to the selected tunable field - - Sets the color value to the first four textboxes/sliders of this field, if less than four textboxes/sliders are available, it will set the values to the available ones and discard all of the value(s) that can't be copied into the field -4) **Name** - - Displays the name of the variable (declared in the pipeline) -5) **Text field** - - The part in which you can modify the value for this field. Can be toggled to sliders as mentioned before - - Some fields (such as OpenCV Scalars) might have more than one text field - -### Tuner configuration - -When opening the config for a specific field with the aforementioned button (figure #2), you'll see this popup: - -
- -1) **Slider range** - - Sets the **range for the sliders**, defaults to 0-255 since that's the most commonly used, especially for color tuning. - - Negative & positive values allowed, decimal values are allowed only if the field is decimal (such as floats or doubles) -2) **Color space** - - Sets the **color space** for the color picker to return. Defaults to RGB -3) **Apply to all fields...** - - Applies **this configuration** globally or specifically to this field *(see below for further details)* -4) **Config source** - - Displays the source of this config: default global, global, local or specific *(see below for further details)* - -#### Applying tuner configuration - -When using the variable tuner and making configurations, it's sometimes convenient to have some way to store those configurations so that you don't have to reconfig for every field, or every time you select a new pipeline.
- -For this, the sim has a "apply to all" functionality to store common configurations: - -
- -As you can see in the image, when clicking the "apply to all" button, two options appear: - -- **"Globally"** - - Applies the configuration **globally** (to all fields without an "specific" config) - - Note that this doesn't mean that by clicking this option will override all configurations, see below. -- **"Of this type"** (or "specific") - - Applies the configuration to all fields **of the same type** as the current config one. - - (In the case of the example in the screenshot, the blur field in DefaultPipeline, this config will be applied to all *int* fields) - -#### Tuner configuration priority order - -As mentioned before, by applying a "global" configuration, it doesn't mean that it will override the specific configs.
-Rather, there's an specific priority order to determine the configuration that will be given to each tunable field: - -1) **Local** - - This is the one that applies when you modify the configuration values without applying *"globally"* or *"specifically to this type"* - - Simply means that you modified the config without saving it. It will be reset once you select a different pipeline -2) **Type-specific** - - If there's a specific configuration for the type *(such as **int** in the example)*, it will be the one that gets the most priority - - You can define a "type-specific" configuration by clicking on "Applying to all fields..." -> "Of this type" -3) **Global** - - If there's not a type-specific configuration present, the configuration will default to the "global" one - - You can define a "Global" configuration by clicking on "Applying to all fields..." -> "Globally" -5) **Tunable field suggestion** - - If there's not a global configuration, but the current tunable field suggests a *"mode" (sliders or textboxes)*, then that suggestion will be applied - - For example, with OpenCV's Scalars, the tunable field suggest to use sliders since it's more convenient for tuning this type of field -6) **Default global** - - If there's not any configuration or suggestion at all, the field will default to the *"default global"* configuration - - The default config has a slider range from 0 to 255 and a color space of RGB - -### Sample usage of the variable tuner - -Let's say we need to tune a threshold for finding the ring stack in the 2020-2021 "Ultimate Goal" game. For this, we will use the YCrCb color space since it's one of the most used ones in FTC and it behaves better under different lightning conditions. (see [this article](https://learnopencv.com/color-spaces-in-opencv-cpp-python/) for more extended explaination and comparation of different color spaces).
- -We can write a simple pipeline for achieving this, taking advantage of the variable tuner. Here's an example code with detailed comments: - -```java -package org.firstinspires.ftc.teamcode; - -import org.opencv.core.Core; -import org.opencv.core.Mat; -import org.opencv.core.Scalar; -import org.opencv.imgproc.Imgproc; -import org.openftc.easyopencv.OpenCvPipeline; - -public class SimpleThresholdPipeline extends OpenCvPipeline { - - /* - * These are our variables that will be - * modifiable from the variable tuner. - * - * Scalars in OpenCV are generally used to - * represent color. So our values in the - * lower and upper Scalars here represent - * the Y, Cr and Cb values respectively. - * - * YCbCr, like most color spaces, range - * from 0-255, so we default to those - * min and max values here for now, meaning - * that all pixels will be shown. - */ - public Scalar lower = new Scalar(0, 0, 0); - public Scalar upper = new Scalar(255, 255, 255); - - /* - * A good practice when typing EOCV pipelines is - * declaring the Mats you will use here at the top - * of your pipeline, to reuse the same buffers every - * time. This removes the need to call mat.release() - * with every Mat you create on the processFrame method, - * and therefore, reducing the possibility of getting a - * memory leak and causing the app to crash due to an - * "Out of Memory" error. - */ - private Mat ycrcbMat = new Mat(); - private Mat binaryMat = new Mat(); - private Mat maskedInputMat = new Mat(); - - @Override - public Mat processFrame(Mat input) { - /* - * Converts our input mat from RGB to YCrCb. - * EOCV ALWAYS returns RGB mats, so you'd - * always convert from RGB to the color - * space you want to use. - * - * Takes our "input" mat as an input, and outputs - * to a separate Mat buffer "ycrcbMat" - */ - Imgproc.cvtColor(input, ycrcbMat, Imgproc.COLOR_RGB2YCrCb); - - /* - * This is where our thresholding actually happens. - * Takes our "ycrcbMat" as input and outputs a "binary" - * Mat to "binaryMat" of the same size as our input. - * "Discards" all the pixels outside the bounds specified - * by the scalars above (and modifiable with EOCV-Sim's - * live variable tuner.) - * - * Binary meaning that we have either a 0 or 255 value - * for every pixel. - * - * 0 represents our pixels that were outside the bounds - * 255 represents our pixels that are inside the bounds - */ - Core.inRange(ycrcbMat, lower, upper, binaryMat); - - /* - * Release the reusable Mat so that old data doesn't - * affect the next step in the current processing - */ - maskedInputMat.release(); - - /* - * Now, with our binary Mat, we perform a "bitwise and" - * to our input image, meaning that we will perform a mask - * which will include the pixels from our input Mat which - * are "255" in our binary Mat (meaning that they're inside - * the range) and will discard any other pixel outside the - * range (RGB 0, 0, 0. All discarded pixels will be black) - */ - Core.bitwise_and(input, input, maskedInputMat, binaryMat); - - /* - * The Mat returned from this method is the - * one displayed on the viewport. - * - * To visualize our threshold, we'll return - * the "masked input mat" which shows the - * pixel from the input Mat that were inside - * the threshold range. - */ - return maskedInputMat; - } - -} -``` - -And so, when initially selecting this pipeline in the simulator, it's initial state should look something like this:
- -
- -All pixels from the input Mat are visible entirely, this is because we specified a range of 0-255 for all three channels (see the sliders values). Since those values are the minimum (0%) and maximum (100%) for YCrCb respectively, all pixels are able to go through our "threshold".
- -Other thing to note here is that we have sliders instead of textboxes for both Scalars. This is the "default" behavior when using a variable of this type, since sliders are the most optimal option to tune thresholds. This behavior can be overriden by any user configuration, by toggling off the button located at the top left with the sliders icon.

-If you want to permanently change this, go into the field config by clicking on the button with the gear icon, then click "apply to all" and select whether you wanna apply this config to all fields globally, or specifically to the field type (Scalar in this case), as explained before.
- -Anyways, back to the sample. After a bit of playing around with the sliders, it's possible to come up with some decent values which successfully filter out the orange ring stack out of everything else:
- -
- -A problem with the YCrCb color space, especially this year, is that the difference between red and orange is very subtle. So therefore we need to play with the values for a good while until we find some that filters out the red from the goals but displays the ring stack. Or do some other technique alongside thresholding such as [FTCLib's contour ring pipeline](https://github.com/FTCLib/FTCLib/blob/3a43b191b18581a2f741588f9b8ab60c13b7fb6c/core/vision/src/main/java/com/arcrobotics/ftclib/vision/UGContourRingPipeline.kt#L46) with the "horizon" mechanism.
- -Some other nice features can be added to this sample, such as an enum for choosing the color space and Telemetry: - -
- -To keep this explaination simple, you can find the final pipeline [here](https://github.com/serivesmejia/EOCV-Sim/blob/dev/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SimpleThresholdPipeline.java) with the new demonstrated features, in the TeamCode module, since serves as a good sample alongside other sample classes from EOCV itself. +# Usage Explanation + +### This guide is still a work in progress + +## Welcome! + +Thank you for your interest in EOCV-Sim :) + +We made this tool in hopes that it will be useful for all FTC teams seeking a way of learning and developing their seasonal OpenCV algorithms in a easy and straightforward way, while also providing some extra tools to improve the experience of developing such algorithms. + +The main purpose of this software is to simulate the package & class structure of OpenFTC's EasyOpenCV and a little bit of the FTC SDK, while also providing OpenCV functionality and a simple GUI. + +By simulating the aforementioned structure, it allows the imports, class names, etc. to be the same as they would if you were using the FTC SDK with EasyOpenCV, allowing you to simply copy paste your vision code onto your Android Studio project once you want to transfer it to a robot.
+ +## Table of Contents +- [Workspaces & VS Code](#workspaces--vs-code) +- [IntelliJ Project Structure (with the old installation method)](#intellij-project-structure-with-the-old-installation-method) + - [Creating pipelines](#creating-pipelines-with-the-old-installation-method) +- [Empty Sample Pipeline](#empty-sample-pipeline) +- [Input Sources](#input-sources) + - [Creating an input source](#creating-an-input-source) +- [Telemetry](#telemetry) +- [Variable Tuner](#variable-tuner) + - [Functionality](#tuner-functionality) + - [Configuration](#tuner-configuration) + - [Sample Usage](#sample-usage-of-the-variable-tuner) + +## Workspaces & VS Code + +**This part is applicable with any installation method** + +### Introduction to workspaces + +Workspaces are the new major feature introduced in v3.0.0! + +A workspace basically consists of a folder containing `.java` source files and resource files, which are compiled on the fly by EOCV-Sim. This removes the need of having to use Gradle for running builds, or even allowing to see code changes in real time within a few seconds or even milliseconds! + +Note that a Java Development Kit (JDK) is needed for using this feature, since Java Runtime Environments (JREs) don't come with a compiler packaged, + +The simulator watches for file changes in the background every 800ms, and triggers a build automatically. This can be greately beneficial for writing pipelines, plus the variable tuner that also allows to change variables in real time to reflect changes immediately. The simulator also tries to snapshot the state of the pipeline selected before the build, and try to apply it to the (possibly new if it was compiled on runtime) selected pipeline if the package and class names of the old and the new pipeline match, therefore values tuned with the variable tuner can be kept between builds even if the source code has changed. + +The simulator creates and selects by default a workspace in the user folder, `~/.eocvsim/default_workspace`, which contains a sample GrayscalePipeline that is compiled and added on runtime, but you can change it by doing the following steps: + +1) Go under the "Pipelines" section, click the "Workspace" and finally "Select workspace". Or alternatively, you can also go to Workspace -> Select workspace + + + +2) Select a folder in the file explorer that pops up + +3) Done! The sim should select the folder as a workspace, create a `eocvsim_workspace.json` file if it doesn't exist, start running a build and watch for file changes in the selected folder + +### VS Code + +### The `eocvsim_workspace.json` file + +## IntelliJ project structure (with the old installation method) + +**This part is only applicable if you downloaded EOCV-Sim with the [old method explained in the README](/README.md#altenative-installation-method-intellij-idea)** + +EOCV-Sim uses Gradle starting from v2.0.0, because of this, the project structure is a bit different. For finding the package in which the pipelines have to be placed:
+1) Pop out the parent EOCV-Sim project folder by clicking on the "*>*" arrow +2) Find the TeamCode module (folder) and pop it out just like before +3) Find the src folder and open it +4) Now you will find the *org.firstinspires.ftc.teamcode* package, in which you should place all your pipelines and some sample pipelines are already there.
+ +These steps are illustrated in this gif:
+ +
+ +### Creating pipelines (with the old installation method) + +**This part is also only applicable if you downloaded EOCV-Sim with the [old method explained in the README](/README.md#altenative-installation-method-intellij-idea)** + +As said before, all of the pipeline classes **should be** placed under the *org.firstinspires.ftc.teamcode* package, in the *TeamCode* module. This way, they will be +automatically detected by the simulator and will be selectionable from the GUI. + +
+ +*(Also, the simulator already comes by default with some EasyOpenCV samples)*
+ +To create a new java class, follow these steps:
+1) In the project files menu, open the TeamCode module +2) Find the *org.firstinspires.ftc.teamcode* package and right click on it +3) On the context menu, click on *New > Java Class* +4) A new menu will appear, type a name and make sure the *Class* option is selected +5) Once you have typed a name, press enter and the class will be created + +Here's a quick gif illustrating these steps:
+ +
+ +## Empty sample pipeline + +If you want your class to be a pipeline, it **should also** extend the EOCV's OpenCvPipeline abstract class and override the processFrame() method.

+Here's a empty pipeline template, with the SamplePipeline class we created before: + +```java +package org.firstinspires.ftc.teamcode; + +import org.opencv.core.Mat; +import org.openftc.easyopencv.OpenCvPipeline; + +public class SamplePipeline extends OpenCvPipeline { + + @Override + public void init(Mat input) { + /* Executed once, when the pipeline is selected */ + } + + @Override + public Mat processFrame(Mat input) { + /* Executed each frame, the returned mat will be the one displayed */ + /* Processing and detection stuff here */ + return input; // Return the input mat + // (Or a new, processed mat) + } + + @Override + public void onViewportTapped() { + /* + * Executed everytime when the pipeline view is tapped/clicked. + * This is executed from the UI thread, so whatever you do here, + * it must be done it quickly. + */ + } + +} +``` + +### For more detailed information about pipelines, make sure to check out the [EasyOpenCV docs](https://github.com/OpenFTC/EasyOpenCV/blob/master/doc/user_docs/pipelines_overview.md) + +## Input Sources + +To allow multiple ways to test your pipeline, the simulator comes with *Input Sources*, which are the ones in charge of giving your pipeline the input Mats, As of right now, the sim has three types of Input Sources: + +- **Image Source:** + - These will feed your pipeline with a static image loaded in your computer's hard drive. + - To save resources, your pipeline will just run once when you select an image source, but you can optionally resume the pipeline execution by clicking the "Pause" button under the pipeline selector. +- **Camera Source:** + - These will feed your pipeline with a constantly changing video stream from a specified camera plugged in your computer. + - Unlike the image sources, these will not pause the execution of you pipeline by default, but you can click the "Pause" button to pause it at any time. +- **Video Source:** + - These will feed your pipeline with a constantly changing video stream from a file in your hard drive, pause rules are the same as camera sources. + - Most tested video format is *\*.avi*, although it depends on your operating system's support. + +### Creating an Input Source + +1) From your Operating System's file manager, grab a media file such as an image or a video. + +## Telemetry + +There's also an SDK-like Telemetry implementation in the sim. +In 1.1.0 (when it was introduced) you could simply access it from your pipeline since it was an instance variable ```telemetry```. + +But, starting 2.0.0, to make it more alike to an actual EOCV pipeline, you need to implement a public constructor which takes a Telemetry parameter, then creating and setting an instance variable from that constructor: + +```java +package org.firstinspires.ftc.teamcode; + +import org.opencv.core.Mat; +import org.openftc.easyopencv.OpenCvPipeline; + +import org.firstinspires.ftc.robotcore.external.Telemetry; + +public class TelemetryPipeline extends OpenCvPipeline { + + Telemetry telemetry; + + public TelemetryPipeline(Telemetry telemetry) { + this.telemetry = telemetry; + } + + @Override + public Mat processFrame(Mat input) { + telemetry.addData("[Hello]", "World!"); + telemetry.update(); + return input; // Return the input mat + } + +} +``` + +Which then produces the following result:
+ +
+ +For further information about telemetry, you can check out the [SDK docs on Telemetry](https://ftctechnh.github.io/ftc_app/doc/javadoc/org/firstinspires/ftc/robotcore/external/Telemetry.html), note that not all the methods are implemented for EOCV-Sim + +## Variable Tuner + +From 2.0.0 and on, there's a variable tuner implemented into the simulator, inspired by the one in FTC Dashboard, it allows to edit public, non-final variables from your pipeline in real time seamlessly through Java reflection.
+ +This variable tuner can be found at the bottom part of the sim, click on the divider bar to open it:
+ +
+
+ +This screenshot is from the DefaultPipeline (the one selected when the simulator opens). This variable controls the blur value for the output Mat. You can play with this value to see the tuner functionality.

+If we look into the DefaultPipeline code, we can see that it is simply a **public int** instance variable, not marked as final (alongside with the Telemetry initialization stuff we explained before):
+ +
+ +The tuner supports a handful of Java types such as most primivites (int, boolean...) and some other types from OpenCV.
+The full list of types currently supported by the tuner on the latest version is:
+ + Java: + - int (or Integer) + - float (or Float) + - double (or Double) + - long (or Long) + - boolean (or Boolean) + - String + - Enums + + OpenCV: + - Scalar + - Rect + - Point + +### Tuner functionality + +In the screenshot above, you might have noticed we have three buttons in the field (these buttons appear on field types with at least one textbox/slider). Those were introduced in 2.1.0 to provide extra functionality to the tuner. We have three buttons (options), five parts: + +
+ +1) **Text fields/slider toggle** + - Toggles between sliders and textboxes for setting the value to this field +2) **Config** + - Configures various aspects of this tunable field, such as the slider range and picker's color space +3) **Color picker** + - Turns on "color picker" mode, which allows to grab a single pixel from the image and sets it to the selected tunable field + - Sets the color value to the first four textboxes/sliders of this field, if less than four textboxes/sliders are available, it will set the values to the available ones and discard all of the value(s) that can't be copied into the field +4) **Name** + - Displays the name of the variable (declared in the pipeline) +5) **Text field** + - The part in which you can modify the value for this field. Can be toggled to sliders as mentioned before + - Some fields (such as OpenCV Scalars) might have more than one text field + +### Tuner configuration + +When opening the config for a specific field with the aforementioned button (figure #2), you'll see this popup: + +
+ +1) **Slider range** + - Sets the **range for the sliders**, defaults to 0-255 since that's the most commonly used, especially for color tuning. + - Negative & positive values allowed, decimal values are allowed only if the field is decimal (such as floats or doubles) +2) **Color space** + - Sets the **color space** for the color picker to return. Defaults to RGB +3) **Apply to all fields...** + - Applies **this configuration** globally or specifically to this field *(see below for further details)* +4) **Config source** + - Displays the source of this config: default global, global, local or specific *(see below for further details)* + +#### Applying tuner configuration + +When using the variable tuner and making configurations, it's sometimes convenient to have some way to store those configurations so that you don't have to reconfig for every field, or every time you select a new pipeline.
+ +For this, the sim has a "apply to all" functionality to store common configurations: + +
+ +As you can see in the image, when clicking the "apply to all" button, two options appear: + +- **"Globally"** + - Applies the configuration **globally** (to all fields without an "specific" config) + - Note that this doesn't mean that by clicking this option will override all configurations, see below. +- **"Of this type"** (or "specific") + - Applies the configuration to all fields **of the same type** as the current config one. + - (In the case of the example in the screenshot, the blur field in DefaultPipeline, this config will be applied to all *int* fields) + +#### Tuner configuration priority order + +As mentioned before, by applying a "global" configuration, it doesn't mean that it will override the specific configs.
+Rather, there's an specific priority order to determine the configuration that will be given to each tunable field: + +1) **Local** + - This is the one that applies when you modify the configuration values without applying *"globally"* or *"specifically to this type"* + - Simply means that you modified the config without saving it. It will be reset once you select a different pipeline +2) **Type-specific** + - If there's a specific configuration for the type *(such as **int** in the example)*, it will be the one that gets the most priority + - You can define a "type-specific" configuration by clicking on "Applying to all fields..." -> "Of this type" +3) **Global** + - If there's not a type-specific configuration present, the configuration will default to the "global" one + - You can define a "Global" configuration by clicking on "Applying to all fields..." -> "Globally" +5) **Tunable field suggestion** + - If there's not a global configuration, but the current tunable field suggests a *"mode" (sliders or textboxes)*, then that suggestion will be applied + - For example, with OpenCV's Scalars, the tunable field suggest to use sliders since it's more convenient for tuning this type of field +6) **Default global** + - If there's not any configuration or suggestion at all, the field will default to the *"default global"* configuration + - The default config has a slider range from 0 to 255 and a color space of RGB + +### Sample usage of the variable tuner + +Let's say we need to tune a threshold for finding the ring stack in the 2020-2021 "Ultimate Goal" game. For this, we will use the YCrCb color space since it's one of the most used ones in FTC and it behaves better under different lightning conditions. (see [this article](https://learnopencv.com/color-spaces-in-opencv-cpp-python/) for more extended explaination and comparation of different color spaces).
+ +We can write a simple pipeline for achieving this, taking advantage of the variable tuner. Here's an example code with detailed comments: + +```java +package org.firstinspires.ftc.teamcode; + +import org.opencv.core.Core; +import org.opencv.core.Mat; +import org.opencv.core.Scalar; +import org.opencv.imgproc.Imgproc; +import org.openftc.easyopencv.OpenCvPipeline; + +public class SimpleThresholdPipeline extends OpenCvPipeline { + + /* + * These are our variables that will be + * modifiable from the variable tuner. + * + * Scalars in OpenCV are generally used to + * represent color. So our values in the + * lower and upper Scalars here represent + * the Y, Cr and Cb values respectively. + * + * YCbCr, like most color spaces, range + * from 0-255, so we default to those + * min and max values here for now, meaning + * that all pixels will be shown. + */ + public Scalar lower = new Scalar(0, 0, 0); + public Scalar upper = new Scalar(255, 255, 255); + + /* + * A good practice when typing EOCV pipelines is + * declaring the Mats you will use here at the top + * of your pipeline, to reuse the same buffers every + * time. This removes the need to call mat.release() + * with every Mat you create on the processFrame method, + * and therefore, reducing the possibility of getting a + * memory leak and causing the app to crash due to an + * "Out of Memory" error. + */ + private Mat ycrcbMat = new Mat(); + private Mat binaryMat = new Mat(); + private Mat maskedInputMat = new Mat(); + + @Override + public Mat processFrame(Mat input) { + /* + * Converts our input mat from RGB to YCrCb. + * EOCV ALWAYS returns RGB mats, so you'd + * always convert from RGB to the color + * space you want to use. + * + * Takes our "input" mat as an input, and outputs + * to a separate Mat buffer "ycrcbMat" + */ + Imgproc.cvtColor(input, ycrcbMat, Imgproc.COLOR_RGB2YCrCb); + + /* + * This is where our thresholding actually happens. + * Takes our "ycrcbMat" as input and outputs a "binary" + * Mat to "binaryMat" of the same size as our input. + * "Discards" all the pixels outside the bounds specified + * by the scalars above (and modifiable with EOCV-Sim's + * live variable tuner.) + * + * Binary meaning that we have either a 0 or 255 value + * for every pixel. + * + * 0 represents our pixels that were outside the bounds + * 255 represents our pixels that are inside the bounds + */ + Core.inRange(ycrcbMat, lower, upper, binaryMat); + + /* + * Release the reusable Mat so that old data doesn't + * affect the next step in the current processing + */ + maskedInputMat.release(); + + /* + * Now, with our binary Mat, we perform a "bitwise and" + * to our input image, meaning that we will perform a mask + * which will include the pixels from our input Mat which + * are "255" in our binary Mat (meaning that they're inside + * the range) and will discard any other pixel outside the + * range (RGB 0, 0, 0. All discarded pixels will be black) + */ + Core.bitwise_and(input, input, maskedInputMat, binaryMat); + + /* + * The Mat returned from this method is the + * one displayed on the viewport. + * + * To visualize our threshold, we'll return + * the "masked input mat" which shows the + * pixel from the input Mat that were inside + * the threshold range. + */ + return maskedInputMat; + } + +} +``` + +And so, when initially selecting this pipeline in the simulator, it's initial state should look something like this:
+ +
+ +All pixels from the input Mat are visible entirely, this is because we specified a range of 0-255 for all three channels (see the sliders values). Since those values are the minimum (0%) and maximum (100%) for YCrCb respectively, all pixels are able to go through our "threshold".
+ +Other thing to note here is that we have sliders instead of textboxes for both Scalars. This is the "default" behavior when using a variable of this type, since sliders are the most optimal option to tune thresholds. This behavior can be overriden by any user configuration, by toggling off the button located at the top left with the sliders icon.

+If you want to permanently change this, go into the field config by clicking on the button with the gear icon, then click "apply to all" and select whether you wanna apply this config to all fields globally, or specifically to the field type (Scalar in this case), as explained before.
+ +Anyways, back to the sample. After a bit of playing around with the sliders, it's possible to come up with some decent values which successfully filter out the orange ring stack out of everything else:
+ +
+ +A problem with the YCrCb color space, especially this year, is that the difference between red and orange is very subtle. So therefore we need to play with the values for a good while until we find some that filters out the red from the goals but displays the ring stack. Or do some other technique alongside thresholding such as [FTCLib's contour ring pipeline](https://github.com/FTCLib/FTCLib/blob/3a43b191b18581a2f741588f9b8ab60c13b7fb6c/core/vision/src/main/java/com/arcrobotics/ftclib/vision/UGContourRingPipeline.kt#L46) with the "horizon" mechanism.
+ +Some other nice features can be added to this sample, such as an enum for choosing the color space and Telemetry: + +
+ +To keep this explaination simple, you can find the final pipeline [here](https://github.com/serivesmejia/EOCV-Sim/blob/dev/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SimpleThresholdPipeline.java) with the new demonstrated features, in the TeamCode module, since serves as a good sample alongside other sample classes from EOCV itself. diff --git a/build.common.gradle b/build.common.gradle index f2485043..128e5e3c 100644 --- a/build.common.gradle +++ b/build.common.gradle @@ -1,12 +1,12 @@ -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - -compileKotlin { - kotlinOptions { - jvmTarget = "1.8" - freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" - useIR = true - } -} +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" + useIR = true + } +} diff --git a/build.gradle b/build.gradle index d125dc5a..1fc96b71 100644 --- a/build.gradle +++ b/build.gradle @@ -1,61 +1,61 @@ -import java.nio.file.Paths -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter - -buildscript { - ext { - kotlin_version = "1.5.10" - kotlinx_coroutines_version = "1.5.0-native-mt" - - env = findProperty('env') == 'release' ? 'release' : 'dev' - - println("Current build is: $env") - } - - repositories { - mavenCentral() - } - - dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -allprojects { - group 'com.github.serivesmejia' - version '3.2.0' - - ext { - standardVersion = version - } - - repositories { - mavenCentral() - } - - tasks.withType(Jar) { - manifest { - attributes['Main-Class'] = 'com.github.serivesmejia.eocvsim.Main' - } - } - - if(env == 'dev') { - String date = DateTimeFormatter.ofPattern( - "yyMMdd-HHmm" - ).format(LocalDateTime.now()) - - String hash = findProperty('hash') - - version += "-dev-${hash ?: date}" - println("Final version of ${project} is $version") - - File libsFolder = Paths.get( - projectDir.absolutePath, 'build', 'libs' - ).toFile() - - for(file in libsFolder.listFiles()) { - if(file.name.contains("dev") && file.name.endsWith(".jar")) - file.delete() - } - } -} +import java.nio.file.Paths +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +buildscript { + ext { + kotlin_version = "1.5.10" + kotlinx_coroutines_version = "1.5.0-native-mt" + + env = findProperty('env') == 'release' ? 'release' : 'dev' + + println("Current build is: $env") + } + + repositories { + mavenCentral() + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + group 'com.github.serivesmejia' + version '3.2.0' + + ext { + standardVersion = version + } + + repositories { + mavenCentral() + } + + tasks.withType(Jar) { + manifest { + attributes['Main-Class'] = 'com.github.serivesmejia.eocvsim.Main' + } + } + + if(env == 'dev') { + String date = DateTimeFormatter.ofPattern( + "yyMMdd-HHmm" + ).format(LocalDateTime.now()) + + String hash = findProperty('hash') + + version += "-dev-${hash ?: date}" + println("Final version of ${project} is $version") + + File libsFolder = Paths.get( + projectDir.absolutePath, 'build', 'libs' + ).toFile() + + for(file in libsFolder.listFiles()) { + if(file.name.contains("dev") && file.name.endsWith(".jar")) + file.delete() + } + } +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f371643e..b65adcfb 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0c..02640cba 100755 --- a/gradlew +++ b/gradlew @@ -1,185 +1,185 @@ -#!/usr/bin/env sh - -# -# Copyright 2015 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -exec "$JAVACMD" "$@" +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 107acd32..ac1b06f9 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,89 +1,89 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/imgui.ini b/imgui.ini index e96823e0..c276244f 100644 --- a/imgui.ini +++ b/imgui.ini @@ -10,6 +10,6 @@ Collapsed=0 [Window][Editor] Pos=0,0 -Size=1366,705 +Size=1366,711 Collapsed=0 diff --git a/jitpack.yml b/jitpack.yml index 29b6f0a2..ad940f7c 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,6 +1,6 @@ -jdk: - - openjdk8 -before_install: - - chmod +x gradlew -install: - - ./gradlew :EOCV-Sim:clean :EOCV-Sim:build :EOCV-Sim:publishToMavenLocal -x :EOCV-Sim:test +jdk: + - openjdk8 +before_install: + - chmod +x gradlew +install: + - ./gradlew :EOCV-Sim:clean :EOCV-Sim:build :EOCV-Sim:publishToMavenLocal -x :EOCV-Sim:test diff --git a/settings.gradle b/settings.gradle index 79e21e29..7e077aa7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,5 +10,5 @@ rootProject.name = 'EOCV-Sim' include 'TeamCode' include 'EOCV-Sim' -include 'NodeEye' +include 'EasyVision' diff --git a/test-logging.gradle b/test-logging.gradle index 3ba87719..3026cc24 100644 --- a/test-logging.gradle +++ b/test-logging.gradle @@ -1,38 +1,38 @@ -import org.gradle.api.tasks.testing.logging.TestExceptionFormat -import org.gradle.api.tasks.testing.logging.TestLogEvent - -test { - testLogging { - // set options for log level LIFECYCLE - events TestLogEvent.FAILED, - TestLogEvent.PASSED, - TestLogEvent.SKIPPED, - TestLogEvent.STANDARD_OUT - exceptionFormat TestExceptionFormat.FULL - showExceptions true - showCauses true - showStackTraces true - - // set options for log level DEBUG and INFO - debug { - events TestLogEvent.STARTED, - TestLogEvent.FAILED, - TestLogEvent.PASSED, - TestLogEvent.SKIPPED, - TestLogEvent.STANDARD_ERROR, - TestLogEvent.STANDARD_OUT - exceptionFormat TestExceptionFormat.FULL - } - info.events = debug.events - info.exceptionFormat = debug.exceptionFormat - - afterSuite { desc, result -> - if (!desc.parent) { // will match the outermost suite - def output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)" - def startItem = '| ', endItem = ' |' - def repeatLength = startItem.length() + output.length() + endItem.length() - println('\n' + ('-' * repeatLength) + '\n' + startItem + output + endItem + '\n' + ('-' * repeatLength)) - } - } - } +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent + +test { + testLogging { + // set options for log level LIFECYCLE + events TestLogEvent.FAILED, + TestLogEvent.PASSED, + TestLogEvent.SKIPPED, + TestLogEvent.STANDARD_OUT + exceptionFormat TestExceptionFormat.FULL + showExceptions true + showCauses true + showStackTraces true + + // set options for log level DEBUG and INFO + debug { + events TestLogEvent.STARTED, + TestLogEvent.FAILED, + TestLogEvent.PASSED, + TestLogEvent.SKIPPED, + TestLogEvent.STANDARD_ERROR, + TestLogEvent.STANDARD_OUT + exceptionFormat TestExceptionFormat.FULL + } + info.events = debug.events + info.exceptionFormat = debug.exceptionFormat + + afterSuite { desc, result -> + if (!desc.parent) { // will match the outermost suite + def output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)" + def startItem = '| ', endItem = ' |' + def repeatLength = startItem.length() + output.length() + endItem.length() + println('\n' + ('-' * repeatLength) + '\n' + startItem + output + endItem + '\n' + ('-' * repeatLength)) + } + } + } } \ No newline at end of file From 611a11adbe336e899b6985535a1e3e9d9b79cea7 Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Sat, 11 Sep 2021 10:22:23 -0600 Subject: [PATCH 14/56] Add changelog for v3.1.0 --- README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 44105f62..c705e7a8 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,20 @@ For bug reporting or feature requesting, use the [issues tab](https://github.com # Change logs -### [v3.0.0 - Compiling on the fly! Yay!](https://github.com/serivesmejia/EOCV-Sim/releases/tag/v3.0.0) +### [v3.1.0 - Better Error Handling](https://github.com/deltacv/EOCV-Sim/releases/tag/v3.1.0) + + - This is the 10th release for EOCV-Sim + + - Changelog: + - Improved pipeline error handling and error output gui + - Build output was improved and unified with the pipeline error output gui + - Added a SplashScreen with the EOCV-Sim logo while the sim loads + - Settings for changing the max FPS of the video recordings and pipelines, and the max pipeline processing time before it's considered "stuck on processFrame" + - Improved camera source creation dialog by providing a list of the available cameras + - Added file locking so that two EOCV-Sim instances can't exist at the same time + - Updated links to reflect the change to the deltacv organization + +### [v3.0.0 - Compiling on the fly! Yay!](https://github.com/deltacv/EOCV-Sim/releases/tag/v3.0.0) - This is the 9th release for EOCV-Sim From cd290b4da67175d1b5a8fac54118f2c971bffebc Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Sat, 11 Sep 2021 17:44:38 -0600 Subject: [PATCH 15/56] Fixed camera creation window on linux --- .../gui/dialog/source/CreateCameraSource.java | 41 +++++++++++++++---- imgui.ini | 2 +- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateCameraSource.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateCameraSource.java index 7bf00cfe..c1639654 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateCameraSource.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/source/CreateCameraSource.java @@ -40,6 +40,8 @@ public class CreateCameraSource { + public static int VISIBLE_CHARACTERS_COMBO_BOX = 22; + public JDialog createCameraSource = null; public JComboBox camerasComboBox = null; @@ -56,7 +58,7 @@ public class CreateCameraSource { JLabel statusLabel = new JLabel(); - enum State { INITIAL, CLICKED_TEST, TEST_SUCCESSFUL, TEST_FAILED } + enum State { INITIAL, CLICKED_TEST, TEST_SUCCESSFUL, TEST_FAILED, NO_WEBCAMS } public CreateCameraSource(JFrame parent, EOCVSim eocvSim) { createCameraSource = new JDialog(parent); @@ -73,7 +75,7 @@ public void initCreateImageSource() { createCameraSource.setModal(true); createCameraSource.setTitle("Create camera source"); - createCameraSource.setSize(350, 250); + createCameraSource.setSize(350, 280); JPanel contentsPanel = new JPanel(new GridLayout(5, 1)); @@ -84,11 +86,22 @@ public void initCreateImageSource() { idLabel.setHorizontalAlignment(JLabel.LEFT); camerasComboBox = new JComboBox<>(); - for(Webcam webcam : webcams) { - camerasComboBox.addItem(webcam.getName()); - } + if(webcams.isEmpty()) { + camerasComboBox.addItem("No Cameras Detected"); + state = State.NO_WEBCAMS; + } else { + for(Webcam webcam : webcams) { + // limit the webcam name to certain characters and append dots in the end if needed + String dots = webcam.getName().length() > VISIBLE_CHARACTERS_COMBO_BOX ? "..." : ""; + + camerasComboBox.addItem( + // https://stackoverflow.com/a/27060643 + String.format("%1." + VISIBLE_CHARACTERS_COMBO_BOX + "s", webcam.getName()).trim() + dots + ); + } - SwingUtilities.invokeLater(() -> camerasComboBox.setSelectedIndex(0)); + SwingUtilities.invokeLater(() -> camerasComboBox.setSelectedIndex(0)); + } idPanel.add(idLabel); idPanel.add(camerasComboBox); @@ -169,7 +182,7 @@ public void initCreateImageSource() { }); camerasComboBox.addActionListener((e) -> { - String sourceName = (String)camerasComboBox.getSelectedItem(); + String sourceName = webcams.get(camerasComboBox.getSelectedIndex()).getName(); if(!eocvSim.inputSourceManager.isNameOnUse(sourceName)) { nameTextField.setText(sourceName); } @@ -196,6 +209,8 @@ public void changed() { close(); }); + updateState(); + createCameraSource.setResizable(false); createCameraSource.setLocationRelativeTo(null); createCameraSource.setVisible(true); @@ -261,6 +276,18 @@ private void updateState() { statusLabel.setText("Failed to open camera, try another one."); createButton.setText("Test"); break; + + case NO_WEBCAMS: + statusLabel.setText("No cameras detected."); + createButton.setText("Test"); + nameTextField.setText(""); + + createButton.setEnabled(false); + nameTextField.setEnabled(false); + camerasComboBox.setEnabled(false); + sizeFieldsInput.getWidthTextField().setEnabled(false); + sizeFieldsInput.getHeightTextField().setEnabled(false); + break; } } diff --git a/imgui.ini b/imgui.ini index c276244f..a8087cef 100644 --- a/imgui.ini +++ b/imgui.ini @@ -10,6 +10,6 @@ Collapsed=0 [Window][Editor] Pos=0,0 -Size=1366,711 +Size=1280,708 Collapsed=0 From a7a1c317dc55d99f0bdbdd4f94435623b094c4de Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Sun, 12 Sep 2021 14:48:29 -0600 Subject: [PATCH 16/56] Renaming and tweaking stuff related to workspaces --- .../eocvsim/gui/DialogFactory.java | 21 ++++++++++++++++ .../serivesmejia/eocvsim/gui/Visualizer.java | 23 +++++++++++++++--- .../gui/component/visualizer/TopMenuBar.kt | 24 ++++++++++++------- .../eocvsim/input/InputSourceManager.java | 22 ++++++++++++++--- .../eocvsim/workspace/WorkspaceManager.kt | 10 +++++++- .../util/template/GradleWorkspaceTemplate.kt | 2 -- 6 files changed, 84 insertions(+), 18 deletions(-) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/DialogFactory.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/DialogFactory.java index d4122d6a..0936cb4b 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/DialogFactory.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/DialogFactory.java @@ -39,11 +39,32 @@ import java.awt.*; import java.io.File; import java.util.ArrayList; +import java.util.function.IntConsumer; public class DialogFactory { private DialogFactory() { } + public static void createYesOrNo(Component parent, String message, String submessage, IntConsumer result) { + JPanel panel = new JPanel(); + + JLabel label1 = new JLabel(message); + panel.add(label1); + + if (!submessage.trim().equals("")) { + JLabel label2 = new JLabel(submessage); + panel.add(label2); + panel.setLayout(new GridLayout(2, 1)); + } + + SwingUtilities.invokeLater(() -> result.accept( + JOptionPane.showConfirmDialog(parent, panel, "Confirm", + JOptionPane.YES_NO_OPTION, + JOptionPane.PLAIN_MESSAGE + ) + )); + } + public static FileChooser createFileChooser(Component parent, FileChooser.Mode mode, FileFilter... filters) { FileChooser fileChooser = new FileChooser(parent, mode, filters); invokeLater(fileChooser::init); diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java index c0ea5e92..b65285ce 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/Visualizer.java @@ -39,6 +39,7 @@ import com.github.serivesmejia.eocvsim.pipeline.compiler.PipelineCompiler; import com.github.serivesmejia.eocvsim.util.Log; import com.github.serivesmejia.eocvsim.util.event.EventHandler; +import com.github.serivesmejia.eocvsim.workspace.util.VSCodeLauncher; import com.github.serivesmejia.eocvsim.workspace.util.template.GradleWorkspaceTemplate; import kotlin.Unit; @@ -414,9 +415,15 @@ public void createVSCodeWorkspace() { if(OPTION == JFileChooser.APPROVE_OPTION) { if(!selectedFile.exists()) selectedFile.mkdir(); - if(selectedFile.isDirectory() && - Objects.requireNonNull(selectedFile.listFiles()).length == 0) { - eocvSim.workspaceManager.createWorkspaceWithTemplateAsync(selectedFile, GradleWorkspaceTemplate.INSTANCE); + if(selectedFile.isDirectory() && selectedFile.listFiles().length == 0) { + eocvSim.workspaceManager.createWorkspaceWithTemplateAsync( + selectedFile, GradleWorkspaceTemplate.INSTANCE, + + () -> { + askOpenVSCode(); + return Unit.INSTANCE; // weird kotlin interop + } + ); } else { asyncPleaseWaitDialog( "The selected directory must be empty", "Select an empty directory or create a new one", @@ -427,6 +434,16 @@ public void createVSCodeWorkspace() { }); } + public void askOpenVSCode() { + DialogFactory.createYesOrNo(frame, "A new workspace was created. Do you wanna open VS Code?", "", + (result) -> { + if(result == 0) { + VSCodeLauncher.INSTANCE.asyncLaunch(eocvSim.workspaceManager.getWorkspaceFile()); + } + } + ); + } + // PLEASE WAIT DIALOGS public boolean pleaseWaitDialog(JDialog diag, String message, String subMessage, String cancelBttText, Dimension size, boolean cancellable, AsyncPleaseWaitDialog apwd, boolean isError) { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt index 061a50ef..f5d0e861 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/visualizer/TopMenuBar.kt @@ -39,6 +39,10 @@ import javax.swing.JMenuItem class TopMenuBar(visualizer: Visualizer, eocvSim: EOCVSim) : JMenuBar() { + companion object { + val docsUrl = URI("https://deltacv.gitbook.io/eocv-sim/") + } + @JvmField val mFileMenu = JMenu("File") @JvmField val mWorkspMenu = JMenu("Workspace") @JvmField val mEditMenu = JMenu("Edit") @@ -108,20 +112,22 @@ class TopMenuBar(visualizer: Visualizer, eocvSim: EOCVSim) : JMenuBar() { mWorkspMenu.addSeparator() - val workspVSCode = JMenu("VS Code") + val workspVSCode = JMenu("External") + + val workspVSCodeCreate = JMenuItem("Create Gradle workspace") + + workspVSCodeCreate.addActionListener { visualizer.createVSCodeWorkspace() } + workspVSCode.add(workspVSCodeCreate) - val workspVSCodeOpen = JMenuItem("Open in current workspace") + workspVSCode.addSeparator() + + val workspVSCodeOpen = JMenuItem("Open VS Code here") workspVSCodeOpen.addActionListener { VSCodeLauncher.asyncLaunch(eocvSim.workspaceManager.workspaceFile) } workspVSCode.add(workspVSCodeOpen) - val workspVSCodeCreate = JMenuItem("Create VS Code workspace") - - workspVSCodeCreate.addActionListener { visualizer.createVSCodeWorkspace() } - workspVSCode.add(workspVSCodeCreate) - mWorkspMenu.add(workspVSCode) add(mWorkspMenu) @@ -136,9 +142,9 @@ class TopMenuBar(visualizer: Visualizer, eocvSim: EOCVSim) : JMenuBar() { // HELP - val helpUsage = JMenuItem("Usage") + val helpUsage = JMenuItem("Documentation") helpUsage.addActionListener { - Desktop.getDesktop().browse(URI("https://github.com/deltacv/EOCV-Sim/blob/master/USAGE.md")) + Desktop.getDesktop().browse(docsUrl) } helpUsage.isEnabled = Desktop.isDesktopSupported() diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java index 3b848fe9..72eeb263 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/input/InputSourceManager.java @@ -52,6 +52,8 @@ public class InputSourceManager { public InputSourceLoader inputSourceLoader = new InputSourceLoader(); public SourceSelectorPanel selectorPanel; + private String defaultSource = ""; + public InputSourceManager(EOCVSim eocvSim) { this.eocvSim = eocvSim; selectorPanel = eocvSim.visualizer.sourceSelectorPanel; @@ -69,7 +71,7 @@ public void init() { createDefaultImgInputSource("/images/ug_1.jpg", "ug_eocvsim_1.jpg", "Ultimate Goal 1 Ring", size); createDefaultImgInputSource("/images/ug_0.jpg", "ug_eocvsim_0.jpg", "Ultimate Goal 0 Ring", size); - setInputSource("Ultimate Goal 4 Ring"); + setInputSource("Ultimate Goal 4 Ring", true); inputSourceLoader.loadInputSourcesFromFile(); @@ -99,13 +101,17 @@ private void createDefaultImgInputSource(String resourcePath, String fileName, S public void update(boolean isPaused) { if(currentInputSource == null) return; - currentInputSource.setPaused(isPaused); try { + currentInputSource.setPaused(isPaused); + Mat m = currentInputSource.update(); if(m != null && !m.empty()) m.copyTo(lastMatFromSource); } catch(Exception ex) { Log.error("InputSourceManager", "Error while processing current source", ex); + Log.warn("InputSourceManager", "Changing to default source"); + + setInputSource(defaultSource); } } @@ -165,7 +171,7 @@ public void addInputSource(String name, InputSource inputSource, boolean dispatc }); } - Log.info("InputSourceManager", "Adding InputSource " + inputSource.toString() + " (" + inputSource.getClass().getSimpleName() + ")"); + Log.info("InputSourceManager", "Adding InputSource " + inputSource + " (" + inputSource.getClass().getSimpleName() + ")"); } public void deleteInputSource(String sourceName) { @@ -180,6 +186,16 @@ public void deleteInputSource(String sourceName) { inputSourceLoader.saveInputSourcesToFile(); } + public boolean setInputSource(String sourceName, boolean makeDefault) { + boolean result = setInputSource(sourceName); + + if(result && makeDefault) { + defaultSource = sourceName; + } + + return result; + } + public boolean setInputSource(String sourceName) { InputSource src = sources.get(sourceName); diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/WorkspaceManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/WorkspaceManager.kt index 9517ecea..4fb6041b 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/WorkspaceManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/WorkspaceManager.kt @@ -32,12 +32,14 @@ import com.github.serivesmejia.eocvsim.util.SysUtil import com.github.serivesmejia.eocvsim.workspace.config.WorkspaceConfig import com.github.serivesmejia.eocvsim.workspace.config.WorkspaceConfigLoader import com.github.serivesmejia.eocvsim.workspace.util.WorkspaceTemplate +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.io.File import java.nio.file.Paths +@OptIn(DelicateCoroutinesApi::class) class WorkspaceManager(val eocvSim: EOCVSim) { companion object { @@ -152,12 +154,18 @@ class WorkspaceManager(val eocvSim: EOCVSim) { return true } - fun createWorkspaceWithTemplateAsync(folder: File, template: WorkspaceTemplate) = GlobalScope.launch(Dispatchers.IO) { + @JvmOverloads fun createWorkspaceWithTemplateAsync( + folder: File, + template: WorkspaceTemplate, + finishCallback: (() -> Unit)? = null + ) = GlobalScope.launch(Dispatchers.IO) { if(!folder.isDirectory) return@launch if(!template.extractToIfEmpty(folder)) return@launch eocvSim.onMainUpdate.doOnce { workspaceFile = folder + if(finishCallback != null) finishCallback() + eocvSim.visualizer.asyncCompilePipelines() } } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/template/GradleWorkspaceTemplate.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/template/GradleWorkspaceTemplate.kt index 4c883b1e..0d8fd93a 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/template/GradleWorkspaceTemplate.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/template/GradleWorkspaceTemplate.kt @@ -53,8 +53,6 @@ object GradleWorkspaceTemplate : WorkspaceTemplate() { Log.info(TAG, "Successfully extracted template") reformatTemplate(folder) //format necessary template files in the folder - - VSCodeLauncher.asyncLaunch(folder) // launch vs code true } catch(ex: IOException) { Log.warn(TAG, "Failed to extract workspace template to ${folder.absolutePath}", ex) From 275ca15e8c8d860f17167f105971a5d745b7592d Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Tue, 14 Sep 2021 10:50:36 -0600 Subject: [PATCH 17/56] Add recursion detector for node editor --- EasyVision/build.gradle | 1 + .../deltacv/easyvision/attribute/Attribute.kt | 5 ++ .../attribute/math/IntegerAttribute.kt | 1 - .../io/github/deltacv/easyvision/node/Link.kt | 48 +++++++++++------ .../io/github/deltacv/easyvision/node/Node.kt | 53 +++++++++++++------ .../deltacv/easyvision/node/NodeEditor.kt | 19 ++++--- .../easyvision/node/math/SumIntegerNode.kt | 1 + .../easyvision/node/vision/InputMatNode.kt | 12 ----- .../vision/{OutputMatNode.kt => MatNodes.kt} | 8 ++- gradlew | 53 +++++++------------ imgui.ini | 2 +- 11 files changed, 116 insertions(+), 87 deletions(-) delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/InputMatNode.kt rename EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/{OutputMatNode.kt => MatNodes.kt} (65%) diff --git a/EasyVision/build.gradle b/EasyVision/build.gradle index b46563ce..b1f3f2d5 100644 --- a/EasyVision/build.gradle +++ b/EasyVision/build.gradle @@ -6,4 +6,5 @@ plugins { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib" implementation "io.github.spair:imgui-java-app:1.84.1.0" + implementation 'com.google.code.gson:gson:2.8.7' } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt index 4fa8574f..ed1408c2 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt @@ -2,6 +2,7 @@ package io.github.deltacv.easyvision.attribute import imgui.extension.imnodes.ImNodes import io.github.deltacv.easyvision.id.DrawableIdElement +import io.github.deltacv.easyvision.node.Link import io.github.deltacv.easyvision.node.Node enum class AttributeMode { INPUT, OUTPUT } @@ -15,6 +16,10 @@ abstract class Attribute : DrawableIdElement { var parentNode: Node? = null internal set + val links = mutableListOf() + + val hasLink get() = links.isNotEmpty() + abstract fun drawAttribute() override fun draw() { diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt index f52a98ba..c6a6e326 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt @@ -4,7 +4,6 @@ import imgui.ImGui import imgui.type.ImInt import io.github.deltacv.easyvision.attribute.AttributeMode import io.github.deltacv.easyvision.attribute.TypedAttribute -import io.github.deltacv.easyvision.node.Link.Companion.hasLink class IntAttribute( override val mode: AttributeMode, diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Link.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Link.kt index df4945b7..c9da23ee 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Link.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Link.kt @@ -7,30 +7,48 @@ import io.github.deltacv.easyvision.id.IdElementContainer class Link(val a: Int, val b: Int) : DrawableIdElement { - companion object { - val links = IdElementContainer() - - fun getLinkOf(attributeId: Int): Link? { - for(link in links) { - if(link.a == attributeId || link.b == attributeId) { - return link - } - } + override val id by links.nextId { this } - return null - } + val aAttrib = Node.attributes[a]!! + val bAttrib = Node.attributes[b]!! - val Attribute.hasLink get() = getLinkOf(id) != null - } + override fun draw() { + if(!aAttrib.links.contains(this)) + aAttrib.links.add(this) - override val id by links.nextId { this } + if(!bAttrib.links.contains(this)) + bAttrib.links.add(this) - override fun draw() { ImNodes.link(id, a, b) } override fun delete() { + aAttrib.links.remove(this) + bAttrib.links.remove(this) + links.removeId(id) } + companion object { + val links = IdElementContainer() + + fun getLinksBetween(a: Node, b: Node): List { + val l = mutableListOf() + + for(link in links) { + val linkNodeA = link.aAttrib.parentNode!! + val linkNodeB = link.bAttrib.parentNode!! + + if ( + (a == linkNodeA && b == linkNodeB) + || (b == linkNodeA && a == linkNodeB) + ) { + l.add(link) + } + } + + return l + } + } + } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt index 6f5c439b..b89ba15f 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt @@ -8,14 +8,6 @@ import io.github.deltacv.easyvision.attribute.AttributeMode abstract class Node(protected var allowDelete: Boolean = true) : DrawableIdElement { - companion object { - val nodes = IdElementContainer() - val attributes = IdElementContainer() - - @JvmStatic protected val INPUT = AttributeMode.INPUT - @JvmStatic protected val OUTPUT = AttributeMode.OUTPUT - } - override val id by nodes.nextId { this } val nodeAttributes = mutableListOf() @@ -33,17 +25,11 @@ abstract class Node(protected var allowDelete: Boolean = true) : DrawableIdEleme override fun delete() { if(allowDelete) { - for(link in Link.links.elements.toTypedArray()) { - for (attribute in nodeAttributes) { - if(link.a == attribute.id || link.b == attribute.id) { - // deleting links that were attached - // to any of this node's attributes - link.delete() - } + for (attribute in nodeAttributes.toTypedArray()) { + for(link in attribute.links.toTypedArray()) { + link.delete() } - } - for (attribute in nodeAttributes.toTypedArray()) { attribute.delete() nodeAttributes.remove(attribute) } @@ -54,4 +40,37 @@ abstract class Node(protected var allowDelete: Boolean = true) : DrawableIdEleme operator fun Attribute.unaryPlus() = nodeAttributes.add(this) + companion object { + val nodes = IdElementContainer() + val attributes = IdElementContainer() + + @JvmStatic protected val INPUT = AttributeMode.INPUT + @JvmStatic protected val OUTPUT = AttributeMode.OUTPUT + + fun checkRecursion(from: Node, to: Node): Boolean { + val linksBetween = Link.getLinksBetween(from, to) + + var hasOutputToInput = false + var hasInputToOutput = false + + for(link in linksBetween) { + val aNode = link.aAttrib.parentNode!! + + val fromAttrib = if(aNode == from) link.aAttrib else link.bAttrib + val toAttrib = if(aNode == to) link.aAttrib else link.bAttrib + + if(!hasOutputToInput) + hasOutputToInput = fromAttrib.mode == OUTPUT && toAttrib.mode == INPUT + + if(!hasInputToOutput) + hasInputToOutput = fromAttrib.mode == INPUT && toAttrib.mode == OUTPUT + + if(hasOutputToInput && hasInputToOutput) + break + } + + return hasOutputToInput && hasInputToOutput + } + } + } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt index 1f4a6344..10a5c1b9 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt @@ -41,16 +41,17 @@ class NodeEditor(val easyVision: EasyVision) { val startAttrib = Node.attributes[start]!! val endAttrib = Node.attributes[end]!! - val input = if(startAttrib.mode == AttributeMode.INPUT) start else end + val input = if(startAttrib.mode == AttributeMode.INPUT) start else end + val output = if(startAttrib.mode == AttributeMode.OUTPUT) start else end val inputAttrib = Node.attributes[input]!! - val outputAttrib = if(startAttrib.mode == AttributeMode.OUTPUT) start else end + val outputAttrib = Node.attributes[output]!! if(startAttrib.mode == endAttrib.mode) { return // linked attributes cannot be of the same mode } - if(!startAttrib.acceptLink(endAttrib) ||!endAttrib.acceptLink(startAttrib)) { + if(!startAttrib.acceptLink(endAttrib) || !endAttrib.acceptLink(startAttrib)) { return // one or both of the attributes didn't accept the link, abort. } @@ -58,10 +59,16 @@ class NodeEditor(val easyVision: EasyVision) { return // we can't link a node to itself! } - val inputLink = Link.getLinkOf(input) - inputLink?.delete() // delete the existing link of the input attribute if there's any + inputAttrib.links.forEach { + it.delete() // delete the existing link(s) of the input attribute if there's any + } + + val link = Link(start, end).enable() // create the link and enable it - Link(start, end).enable() // create the link and enable it + if(Node.checkRecursion(inputAttrib.parentNode!!, outputAttrib.parentNode!!)) { + // remove the link if a recursion case was detected (e.g both nodes were attached to each other) + link.delete() + } } } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt index 7027aa60..a062e372 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt @@ -10,6 +10,7 @@ class SumIntegerNode : DrawNode("Sum Integer") { + IntAttribute(INPUT, "B") + IntAttribute(OUTPUT, "Result") + + IntAttribute(OUTPUT, "Result") } } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/InputMatNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/InputMatNode.kt deleted file mode 100644 index ac138943..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/InputMatNode.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.github.deltacv.easyvision.node.vision - -import io.github.deltacv.easyvision.node.DrawNode -import io.github.deltacv.easyvision.attribute.vision.MatAttribute - -class InputMatNode : DrawNode("Pipeline Input", allowDelete = false) { - - override fun onEnable() { - + MatAttribute(OUTPUT, "Input") - } - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/OutputMatNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt similarity index 65% rename from EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/OutputMatNode.kt rename to EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt index 114fa459..d39c6a1a 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/OutputMatNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt @@ -3,10 +3,14 @@ package io.github.deltacv.easyvision.node.vision import io.github.deltacv.easyvision.node.DrawNode import io.github.deltacv.easyvision.attribute.vision.MatAttribute -class OutputMatNode : DrawNode("Pipeline Output", allowDelete = false) { +class InputMatNode : DrawNode("Pipeline Input", allowDelete = false) { + override fun onEnable() { + + MatAttribute(OUTPUT, "Input") + } +} +class OutputMatNode : DrawNode("Pipeline Output", allowDelete = false) { override fun onEnable() { + MatAttribute(INPUT, "Output") } - } \ No newline at end of file diff --git a/gradlew b/gradlew index 02640cba..518efffc 100755 --- a/gradlew +++ b/gradlew @@ -1,21 +1,5 @@ #!/usr/bin/env sh -# -# Copyright 2015 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - ############################################################################## ## ## Gradle start up script for UN*X @@ -44,7 +28,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -82,7 +66,6 @@ esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then @@ -126,11 +109,10 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath @@ -156,19 +138,19 @@ if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then else eval `echo args$i`="\"$arg\"" fi - i=`expr $i + 1` + i=$((i+1)) done case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -177,9 +159,14 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=`save "$@"` +APP_ARGS=$(save "$@") # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + exec "$JAVACMD" "$@" diff --git a/imgui.ini b/imgui.ini index a8087cef..c276244f 100644 --- a/imgui.ini +++ b/imgui.ini @@ -10,6 +10,6 @@ Collapsed=0 [Window][Editor] Pos=0,0 -Size=1280,708 +Size=1366,711 Collapsed=0 From 53b2398811afa8f39fa5cd380a55cc78c27f9879 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Tue, 14 Sep 2021 19:48:28 -0600 Subject: [PATCH 18/56] Add list attribute that accepts any type --- .../deltacv/easyvision/attribute/Attribute.kt | 12 ++- .../easyvision/attribute/TextBoxAttribute.kt | 9 -- .../easyvision/attribute/TypedAttribute.kt | 26 +++++- .../attribute/math/IntegerAttribute.kt | 11 ++- .../attribute/misc/ListAttribute.kt | 91 +++++++++++++++++++ .../attribute/vision/MatAttribute.kt | 11 ++- .../github/deltacv/easyvision/id/IdElement.kt | 2 + .../easyvision/id/IdElementContainer.kt | 7 ++ .../io/github/deltacv/easyvision/node/Link.kt | 14 ++- .../io/github/deltacv/easyvision/node/Node.kt | 36 ++++++-- .../deltacv/easyvision/node/NodeEditor.kt | 2 +- .../easyvision/node/math/SumIntegerNode.kt | 7 +- .../serialization/NodeSerializer.kt | 4 + imgui.ini | 2 +- 14 files changed, 202 insertions(+), 32 deletions(-) delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TextBoxAttribute.kt create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/serialization/NodeSerializer.kt diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt index ed1408c2..82635ea6 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt @@ -13,11 +13,10 @@ abstract class Attribute : DrawableIdElement { override val id by Node.attributes.nextId { this } - var parentNode: Node? = null + lateinit var parentNode: Node internal set val links = mutableListOf() - val hasLink get() = links.isNotEmpty() abstract fun drawAttribute() @@ -40,6 +39,15 @@ abstract class Attribute : DrawableIdElement { override fun delete() { Node.attributes.removeId(id) + + for(link in links.toTypedArray()) { + link.delete() + links.remove(link) + } + } + + override fun restore() { + Node.attributes[id] = this } abstract fun acceptLink(other: Attribute): Boolean diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TextBoxAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TextBoxAttribute.kt deleted file mode 100644 index edda973d..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TextBoxAttribute.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.deltacv.easyvision.attribute - -abstract class TextBoxAttribute(typeName: String) : TypedAttribute(typeName) { - - override fun drawAttribute() { - super.drawAttribute() - } - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt index f55bc901..d7bf7c7f 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt @@ -2,14 +2,34 @@ package io.github.deltacv.easyvision.attribute import imgui.ImGui -abstract class TypedAttribute(var typeName: String) : Attribute() { +interface Type { + val name: String + val allowsNew: Boolean get() = true + + fun new(mode: AttributeMode, variableName: String): TypedAttribute +} + +abstract class TypedAttribute(var type: Type) : Attribute() { abstract var variableName: String? - protected val finalVarName get() = variableName ?: if(mode == AttributeMode.INPUT) "Input" else "Output" + var drawDescriptiveText = true + var drawType = true + + private val finalVarName by lazy { + variableName ?: if (mode == AttributeMode.INPUT) "Input" else "Output" + } override fun drawAttribute() { - ImGui.text("($typeName) $finalVarName") + if(drawDescriptiveText) { + val t = if(drawType) { + "(${type.name}) " + } else "" + + ImGui.text("$t$finalVarName") + } else { + ImGui.text("") + } } override fun acceptLink(other: Attribute) = this::class == other::class diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt index c6a6e326..c2db1d52 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt @@ -3,12 +3,19 @@ package io.github.deltacv.easyvision.attribute.math import imgui.ImGui import imgui.type.ImInt import io.github.deltacv.easyvision.attribute.AttributeMode +import io.github.deltacv.easyvision.attribute.Type import io.github.deltacv.easyvision.attribute.TypedAttribute class IntAttribute( override val mode: AttributeMode, override var variableName: String? = null -) : TypedAttribute("Int") { +) : TypedAttribute(Companion) { + + companion object: Type { + override val name = "Int" + + override fun new(mode: AttributeMode, variableName: String) = IntAttribute(mode, variableName) + } val value = ImInt() @@ -16,6 +23,8 @@ class IntAttribute( super.drawAttribute() if(!hasLink && mode == AttributeMode.INPUT) { + ImGui.sameLine() + ImGui.pushItemWidth(110.0f) ImGui.inputInt("", value) ImGui.popItemWidth() diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt new file mode 100644 index 00000000..79c4dbf8 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt @@ -0,0 +1,91 @@ +package io.github.deltacv.easyvision.attribute.misc + +import imgui.ImGui +import io.github.deltacv.easyvision.attribute.Attribute +import io.github.deltacv.easyvision.attribute.AttributeMode +import io.github.deltacv.easyvision.attribute.Type +import io.github.deltacv.easyvision.attribute.TypedAttribute + +class ListAttribute( + override val mode: AttributeMode, + val elementType: Type, + override var variableName: String? = null +) : TypedAttribute(Companion) { + + companion object: Type { + override val name = "List" + override val allowsNew = false + + override fun new(mode: AttributeMode, variableName: String): TypedAttribute { + throw UnsupportedOperationException("Cannot instantiate a List attribute with new") + } + } + + val listAttributes = mutableListOf() + + private var beforeHasLink = false + private var firstDraw = false + + override fun draw() { + super.draw() + + for(attrib in listAttributes) { + if(beforeHasLink != hasLink) { + if(hasLink) { + // delete attributes if a link has been created + attrib.delete() + } else { + // restore list attribs if they were previously deleted + // after destroying a link with another node + attrib.restore() + } + } + + if(!hasLink) { // only draw attributes if there's not a link attached + attrib.draw() + } + } + + beforeHasLink = hasLink + } + + override fun drawAttribute() { + ImGui.text("[${elementType.name}] $variableName") + + if(!hasLink && elementType.allowsNew && mode == AttributeMode.INPUT) { + // idk wat the frame height is, i just stole it from + // https://github.com/ocornut/imgui/blob/7b8bc864e9af6c6c9a22125d65595d526ba674c5/imgui_widgets.cpp#L3439 + val buttonSize = ImGui.getFrameHeight() + + ImGui.sameLine() + + if(ImGui.button("+", buttonSize, buttonSize)) { // creates a new element with the + button + // uses the "new" function from the attribute's companion Type + val count = listAttributes.size.toString() + val elementName = count + if(count.length == 1) " " else "" + + val element = elementType.new(AttributeMode.INPUT, elementName) + element.enable() //enables the new element + + element.parentNode = parentNode + element.drawType = false // hides the variable type + + listAttributes.add(element) + } + + // display the - button only if the attributes list is not empty + if(listAttributes.isNotEmpty()) { + ImGui.sameLine() + + if(ImGui.button("-", buttonSize, buttonSize)) { + // remove the last element from the list when - is pressed + listAttributes.removeLastOrNull() + ?.delete() // also delete it from the element id registry + } + } + } + } + + override fun acceptLink(other: Attribute) = other is ListAttribute && other.elementType == elementType + +} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/MatAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/MatAttribute.kt index 6c94f49d..560010fb 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/MatAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/MatAttribute.kt @@ -2,8 +2,17 @@ package io.github.deltacv.easyvision.attribute.vision import io.github.deltacv.easyvision.attribute.TypedAttribute import io.github.deltacv.easyvision.attribute.AttributeMode +import io.github.deltacv.easyvision.attribute.Type class MatAttribute( override val mode: AttributeMode, override var variableName: String? = null -) : TypedAttribute("Image") \ No newline at end of file +) : TypedAttribute(Companion) { + + companion object: Type { + override val name = "Image" + + override fun new(mode: AttributeMode, variableName: String) = MatAttribute(mode, variableName) + } + +} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/id/IdElement.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/id/IdElement.kt index 86c7c3c2..a2a4aef1 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/id/IdElement.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/id/IdElement.kt @@ -10,6 +10,8 @@ interface DrawableIdElement : IdElement { fun delete() + fun restore() + fun onEnable() { } fun enable(): DrawableIdElement { diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/id/IdElementContainer.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/id/IdElementContainer.kt index f5979e9b..1aa17a09 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/id/IdElementContainer.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/id/IdElementContainer.kt @@ -33,5 +33,12 @@ class IdElementContainer : Iterable { operator fun get(id: Int) = e[id] + operator fun set(id: Int, element: T) { + e[id] = element + + if(!elements.contains(element)) + elements.add(element) + } + override fun iterator() = elements.listIterator() } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Link.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Link.kt index c9da23ee..c29ced68 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Link.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Link.kt @@ -29,6 +29,13 @@ class Link(val a: Int, val b: Int) : DrawableIdElement { links.removeId(id) } + override fun restore() { + links[id] = this + + aAttrib.links.add(this) + bAttrib.links.add(this) + } + companion object { val links = IdElementContainer() @@ -36,12 +43,11 @@ class Link(val a: Int, val b: Int) : DrawableIdElement { val l = mutableListOf() for(link in links) { - val linkNodeA = link.aAttrib.parentNode!! - val linkNodeB = link.bAttrib.parentNode!! + val linkNodeA = link.aAttrib.parentNode + val linkNodeB = link.bAttrib.parentNode if ( - (a == linkNodeA && b == linkNodeB) - || (b == linkNodeA && a == linkNodeB) + (a == linkNodeA && b == linkNodeB) || (b == linkNodeA && a == linkNodeB) ) { l.add(link) } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt index b89ba15f..531238e8 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt @@ -6,15 +6,19 @@ import io.github.deltacv.easyvision.id.IdElementContainer import io.github.deltacv.easyvision.attribute.Attribute import io.github.deltacv.easyvision.attribute.AttributeMode -abstract class Node(protected var allowDelete: Boolean = true) : DrawableIdElement { +interface Type { + val name: String +} + +abstract class Node(private var allowDelete: Boolean = true) : DrawableIdElement { override val id by nodes.nextId { this } - val nodeAttributes = mutableListOf() + private val attribs = mutableListOf() // internal mutable list + val nodeAttributes = attribs as List // public read-only protected fun drawAttributes() { for((i, attribute) in nodeAttributes.withIndex()) { - attribute.parentNode = this attribute.draw() if(i < nodeAttributes.size - 1) { @@ -31,14 +35,34 @@ abstract class Node(protected var allowDelete: Boolean = true) : DrawableIdEleme } attribute.delete() - nodeAttributes.remove(attribute) + attribs.remove(attribute) } nodes.removeId(id) } } - operator fun Attribute.unaryPlus() = nodeAttributes.add(this) + override fun restore() { + if(allowDelete) { + for (attribute in nodeAttributes.toTypedArray()) { + for(link in attribute.links.toTypedArray()) { + link.restore() + } + + attribute.restore() + attribs.add(attribute) + } + + nodes[id] = this + } + } + + fun addAttribute(attribute: Attribute) { + attribute.parentNode = this + attribs.add(attribute) + } + + operator fun Attribute.unaryPlus() = addAttribute(this) companion object { val nodes = IdElementContainer() @@ -54,7 +78,7 @@ abstract class Node(protected var allowDelete: Boolean = true) : DrawableIdEleme var hasInputToOutput = false for(link in linksBetween) { - val aNode = link.aAttrib.parentNode!! + val aNode = link.aAttrib.parentNode val fromAttrib = if(aNode == from) link.aAttrib else link.bAttrib val toAttrib = if(aNode == to) link.aAttrib else link.bAttrib diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt index 10a5c1b9..3ef92134 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt @@ -65,7 +65,7 @@ class NodeEditor(val easyVision: EasyVision) { val link = Link(start, end).enable() // create the link and enable it - if(Node.checkRecursion(inputAttrib.parentNode!!, outputAttrib.parentNode!!)) { + if(Node.checkRecursion(inputAttrib.parentNode, outputAttrib.parentNode)) { // remove the link if a recursion case was detected (e.g both nodes were attached to each other) link.delete() } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt index a062e372..a0d21341 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt @@ -2,15 +2,14 @@ package io.github.deltacv.easyvision.node.math import io.github.deltacv.easyvision.node.DrawNode import io.github.deltacv.easyvision.attribute.math.IntAttribute +import io.github.deltacv.easyvision.attribute.misc.ListAttribute class SumIntegerNode : DrawNode("Sum Integer") { override fun onEnable() { - + IntAttribute(INPUT, "A") - + IntAttribute(INPUT, "B") + + ListAttribute(INPUT, IntAttribute, "Numbers") - + IntAttribute(OUTPUT, "Result") - + IntAttribute(OUTPUT, "Result") + + IntAttribute(OUTPUT,"Result") } } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/serialization/NodeSerializer.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/serialization/NodeSerializer.kt new file mode 100644 index 00000000..903d5b07 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/serialization/NodeSerializer.kt @@ -0,0 +1,4 @@ +package io.github.deltacv.easyvision.serialization + +class NodeSerializer { +} \ No newline at end of file diff --git a/imgui.ini b/imgui.ini index c276244f..a8087cef 100644 --- a/imgui.ini +++ b/imgui.ini @@ -10,6 +10,6 @@ Collapsed=0 [Window][Editor] Pos=0,0 -Size=1366,711 +Size=1280,708 Collapsed=0 From 6a89c050c3b22b34a6c78b4800548dcbcbd328fe Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Wed, 15 Sep 2021 17:09:47 -0600 Subject: [PATCH 19/56] Add informative popups/tooltips --- .../github/deltacv/easyvision/EasyVision.kt | 24 +++-- .../github/deltacv/easyvision/PopupBuilder.kt | 92 +++++++++++++++++++ .../easyvision/attribute/TypedAttribute.kt | 6 +- .../attribute/math/BooleanAttribute.kt | 31 +++++++ .../attribute/misc/EnumAttribute.kt | 37 ++++++++ .../attribute/misc/ListAttribute.kt | 11 +-- .../deltacv/easyvision/node/NodeEditor.kt | 3 + .../easyvision/node/math/SumIntegerNode.kt | 1 + .../easyvision/node/vision/CvtColorNode.kt | 20 ++++ .../deltacv/easyvision/util/ElapsedTime.kt | 15 +++ imgui.ini | 2 +- 11 files changed, 226 insertions(+), 16 deletions(-) create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/PopupBuilder.kt create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/BooleanAttribute.kt create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/EnumAttribute.kt create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/util/ElapsedTime.kt diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt index bac8f81d..0191c499 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt @@ -8,6 +8,7 @@ import imgui.flag.ImGuiCond import imgui.flag.ImGuiWindowFlags import io.github.deltacv.easyvision.node.NodeEditor import io.github.deltacv.easyvision.node.math.SumIntegerNode +import io.github.deltacv.easyvision.node.vision.CvtColorNode import io.github.deltacv.easyvision.node.vision.InputMatNode import io.github.deltacv.easyvision.node.vision.OutputMatNode import org.lwjgl.BufferUtils @@ -18,16 +19,20 @@ import org.lwjgl.glfw.GLFWKeyCallback class EasyVision : Application() { - private val w = BufferUtils.createIntBuffer(1) - private val h = BufferUtils.createIntBuffer(1) + companion object { + private var ptr = 0L - val windowSize: ImVec2 get() { - w.position(0) - h.position(0) + private val w = BufferUtils.createIntBuffer(1) + private val h = BufferUtils.createIntBuffer(1) - glfwGetWindowSize(handle, w, h) + val windowSize: ImVec2 get() { + w.position(0) + h.position(0) - return ImVec2(w.get(0).toFloat(), h.get(0).toFloat()) + glfwGetWindowSize(ptr, w, h) + + return ImVec2(w.get(0).toFloat(), h.get(0).toFloat()) + } } private var prevKeyCallback: GLFWKeyCallback? = null @@ -43,6 +48,8 @@ class EasyVision : Application() { SumIntegerNode().enable() SumIntegerNode().enable() + CvtColorNode().enable() + launch(this) editor.destroy() @@ -54,6 +61,7 @@ class EasyVision : Application() { override fun process() { if(prevKeyCallback == null) { + ptr = handle // register a new key callback that will call the previous callback and handle some special keys prevKeyCallback = glfwSetKeyCallback(handle, ::keyCallback) } @@ -71,6 +79,8 @@ class EasyVision : Application() { ImGui.end() + PopupBuilder.draw() + isDeleteReleased = false } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/PopupBuilder.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/PopupBuilder.kt new file mode 100644 index 00000000..ee1cdc66 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/PopupBuilder.kt @@ -0,0 +1,92 @@ +package io.github.deltacv.easyvision + +import imgui.ImGui +import io.github.deltacv.easyvision.util.ElapsedTime + +object PopupBuilder { + + private val tooltips = mutableListOf() + + private val labels = mutableMapOf() + + fun addWarningToolTip(message: String, w: Float? = null, h: Float? = null) { + deleteLabel("WARN") + + val windowSize = EasyVision.windowSize + + var x = windowSize.x * 0.5f + var y = windowSize.y * 0.85f + + val wW = w ?: message.length * 7.5f + val wH = h ?: 30f + + x -= wW / 2f + y += wH / 2f + + addToolTip(x, y, wW, wH, message, 6.0, label = "WARN") + } + + fun addToolTip(x: Float, y: Float, w: Float? = null, h: Float? = null, + message: String, time: Double? = null, label: String = "") { + addToolTip(x, y, w, h, time, label) { + ImGui.text(message) + } + } + + + fun addToolTip(x: Float, y: Float, w: Float? = null, h: Float? = null, + time: Double? = null, label: String = "", drawCallback: () -> Unit) { + val tooltip = ToolTip(x, y, w, h, time, drawCallback) + tooltips.add(tooltip) + + labels[label] = Label(tooltip) { + tooltips.remove(tooltip) + } + } + + fun deleteLabel(label: String) { + labels[label]?.deleteCall?.invoke() + labels.remove(label) + } + + fun draw() { + for(tooltip in tooltips.toTypedArray()) { + if(tooltip.time != null && tooltip.elapsedTime.seconds >= tooltip.time) { + tooltips.remove(tooltip) + + for(label in labels.values.toTypedArray()) { + if(label.any == tooltip) { + label.deleteCall() + } + } + + continue + } + + tooltip.draw() + } + } + + private data class ToolTip(val x: Float, val y: Float, val w: Float?, val h: Float?, + val time: Double?, val callback: () -> Unit) { + + val elapsedTime by lazy { ElapsedTime() } + + fun draw() { + elapsedTime.seconds + + ImGui.setNextWindowPos(x, y) + if(w != null && h != null) { + ImGui.setNextWindowSize(w, h) + } + + ImGui.beginTooltip() + callback() + ImGui.endTooltip() + } + + } + + private data class Label(val any: Any, val deleteCall: () -> Unit) + +} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt index d7bf7c7f..6afd8502 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt @@ -6,7 +6,9 @@ interface Type { val name: String val allowsNew: Boolean get() = true - fun new(mode: AttributeMode, variableName: String): TypedAttribute + fun new(mode: AttributeMode, variableName: String): TypedAttribute { + throw UnsupportedOperationException("Cannot instantiate a List attribute with new") + } } abstract class TypedAttribute(var type: Type) : Attribute() { @@ -16,7 +18,7 @@ abstract class TypedAttribute(var type: Type) : Attribute() { var drawDescriptiveText = true var drawType = true - private val finalVarName by lazy { + protected val finalVarName by lazy { variableName ?: if (mode == AttributeMode.INPUT) "Input" else "Output" } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/BooleanAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/BooleanAttribute.kt new file mode 100644 index 00000000..fb6c2bb2 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/BooleanAttribute.kt @@ -0,0 +1,31 @@ +package io.github.deltacv.easyvision.attribute.math + +import imgui.ImGui +import imgui.type.ImBoolean +import imgui.type.ImInt +import io.github.deltacv.easyvision.attribute.AttributeMode +import io.github.deltacv.easyvision.attribute.Type +import io.github.deltacv.easyvision.attribute.TypedAttribute + +class BooleanAttribute( + override val mode: AttributeMode, + override var variableName: String? = null +) : TypedAttribute(Companion) { + + companion object: Type { + override val name = "Boolean" + + override fun new(mode: AttributeMode, variableName: String) = BooleanAttribute(mode, variableName) + } + + val value = ImBoolean() + + override fun drawAttribute() { + super.drawAttribute() + + if(!hasLink && mode == AttributeMode.INPUT) { + ImGui.checkbox("", value) + } + } + +} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/EnumAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/EnumAttribute.kt new file mode 100644 index 00000000..d69d7e6e --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/EnumAttribute.kt @@ -0,0 +1,37 @@ +package io.github.deltacv.easyvision.attribute.misc + +import imgui.ImGui +import imgui.type.ImInt +import io.github.deltacv.easyvision.attribute.Attribute +import io.github.deltacv.easyvision.attribute.AttributeMode +import io.github.deltacv.easyvision.attribute.Type +import io.github.deltacv.easyvision.attribute.TypedAttribute + +class EnumAttribute>( + override val mode: AttributeMode, + val values: Array, + override var variableName: String? +) : TypedAttribute(Companion) { + + companion object: Type { + override val name = "Enum" + override val allowsNew = false + } + + private val valuesStrings = values.map { + it.name + }.toTypedArray() + + val currentItem = ImInt() + + override fun drawAttribute() { + super.drawAttribute() + + ImGui.pushItemWidth(110.0f) + ImGui.combo("", currentItem, valuesStrings) + ImGui.popItemWidth() + } + + override fun acceptLink(other: Attribute) = other is EnumAttribute<*> && values[0]::class == other.values[0]::class + +} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt index 79c4dbf8..1a58ed0b 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt @@ -15,10 +15,6 @@ class ListAttribute( companion object: Type { override val name = "List" override val allowsNew = false - - override fun new(mode: AttributeMode, variableName: String): TypedAttribute { - throw UnsupportedOperationException("Cannot instantiate a List attribute with new") - } } val listAttributes = mutableListOf() @@ -55,9 +51,12 @@ class ListAttribute( if(!hasLink && elementType.allowsNew && mode == AttributeMode.INPUT) { // idk wat the frame height is, i just stole it from // https://github.com/ocornut/imgui/blob/7b8bc864e9af6c6c9a22125d65595d526ba674c5/imgui_widgets.cpp#L3439 + val buttonSize = ImGui.getFrameHeight() - ImGui.sameLine() + val style = ImGui.getStyle() + + ImGui.sameLine(0.0f, style.itemInnerSpacingX * 2.0f) if(ImGui.button("+", buttonSize, buttonSize)) { // creates a new element with the + button // uses the "new" function from the attribute's companion Type @@ -75,7 +74,7 @@ class ListAttribute( // display the - button only if the attributes list is not empty if(listAttributes.isNotEmpty()) { - ImGui.sameLine() + ImGui.sameLine(0.0f, style.itemInnerSpacingX) if(ImGui.button("-", buttonSize, buttonSize)) { // remove the last element from the list when - is pressed diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt index 3ef92134..a89095b9 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt @@ -5,6 +5,7 @@ import imgui.extension.imnodes.ImNodes import imgui.flag.ImGuiMouseButton import imgui.type.ImInt import io.github.deltacv.easyvision.EasyVision +import io.github.deltacv.easyvision.PopupBuilder import io.github.deltacv.easyvision.attribute.AttributeMode class NodeEditor(val easyVision: EasyVision) { @@ -52,6 +53,7 @@ class NodeEditor(val easyVision: EasyVision) { } if(!startAttrib.acceptLink(endAttrib) || !endAttrib.acceptLink(startAttrib)) { + PopupBuilder.addWarningToolTip("Couldn't link nodes: Types didn't match") return // one or both of the attributes didn't accept the link, abort. } @@ -66,6 +68,7 @@ class NodeEditor(val easyVision: EasyVision) { val link = Link(start, end).enable() // create the link and enable it if(Node.checkRecursion(inputAttrib.parentNode, outputAttrib.parentNode)) { + PopupBuilder.addWarningToolTip("Couldn't link nodes: Recursion problem detected") // remove the link if a recursion case was detected (e.g both nodes were attached to each other) link.delete() } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt index a0d21341..06696d2c 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt @@ -8,6 +8,7 @@ class SumIntegerNode : DrawNode("Sum Integer") { override fun onEnable() { + ListAttribute(INPUT, IntAttribute, "Numbers") + + ListAttribute(OUTPUT, IntAttribute, "A") + IntAttribute(OUTPUT,"Result") } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt new file mode 100644 index 00000000..c8bdb9f2 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt @@ -0,0 +1,20 @@ +package io.github.deltacv.easyvision.node.vision + +import io.github.deltacv.easyvision.attribute.misc.EnumAttribute +import io.github.deltacv.easyvision.attribute.vision.MatAttribute +import io.github.deltacv.easyvision.node.DrawNode + +enum class CvtColors { + RGB, BGR, HSV, YCrCb, LAB +} + +class CvtColorNode : DrawNode("Convert Color") { + + override fun onEnable() { + + MatAttribute(INPUT, "Input") + + EnumAttribute(INPUT, CvtColors.values(), "Convert To") + + + MatAttribute(OUTPUT, "Output") + } + +} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/util/ElapsedTime.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/util/ElapsedTime.kt new file mode 100644 index 00000000..2bd57deb --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/util/ElapsedTime.kt @@ -0,0 +1,15 @@ +package io.github.deltacv.easyvision.util + +class ElapsedTime { + + var startTime = System.currentTimeMillis() + private set + + val millis get() = System.currentTimeMillis() - startTime + val seconds get() = millis.toDouble() / 1000.0 + + fun reset() { + startTime = System.currentTimeMillis() + } + +} \ No newline at end of file diff --git a/imgui.ini b/imgui.ini index a8087cef..c276244f 100644 --- a/imgui.ini +++ b/imgui.ini @@ -10,6 +10,6 @@ Collapsed=0 [Window][Editor] Pos=0,0 -Size=1280,708 +Size=1366,711 Collapsed=0 From 7e899d368488280dd4236cb5cff2bcd3e5d77949 Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Thu, 23 Sep 2021 08:56:19 -0600 Subject: [PATCH 20/56] Update OpenCV to 4.5.3 --- TeamCode/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TeamCode/build.gradle b/TeamCode/build.gradle index cf763982..1444caba 100644 --- a/TeamCode/build.gradle +++ b/TeamCode/build.gradle @@ -6,7 +6,7 @@ plugins { apply from: '../build.common.gradle' dependencies { - implementation 'org.openpnp:opencv:4.3.0-2' + implementation 'org.openpnp:opencv:4.5.3-0' implementation project(':EOCV-Sim') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" @@ -15,4 +15,4 @@ dependencies { task(runSim, dependsOn: 'classes', type: JavaExec) { main = 'com.github.serivesmejia.eocvsim.Main' classpath = sourceSets.main.runtimeClasspath -} \ No newline at end of file +} From 461906d7a5c6e55064863e8b6c20b613253edd7c Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Thu, 23 Sep 2021 08:56:46 -0600 Subject: [PATCH 21/56] Update OpenCV --- EOCV-Sim/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EOCV-Sim/build.gradle b/EOCV-Sim/build.gradle index 2a8bc68f..8e84e6ad 100644 --- a/EOCV-Sim/build.gradle +++ b/EOCV-Sim/build.gradle @@ -36,7 +36,7 @@ apply from: '../test-logging.gradle' dependencies { implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' - implementation 'org.openpnp:opencv:4.3.0-2' + implementation 'org.openpnp:opencv:4.5.3-0' implementation 'com.github.sarxos:webcam-capture:0.3.12' implementation 'info.picocli:picocli:4.6.1' @@ -83,4 +83,4 @@ task(writeBuildClassJava) { "}" } -build.dependsOn writeBuildClassJava \ No newline at end of file +build.dependsOn writeBuildClassJava From 8482eaeaad34afaba7734f4803d5a4859a873642 Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Thu, 23 Sep 2021 14:32:14 -0600 Subject: [PATCH 22/56] Delete USAGE.md to introduce gitbook docs --- USAGE.md | 420 ------------------------------------------------------- 1 file changed, 420 deletions(-) delete mode 100644 USAGE.md diff --git a/USAGE.md b/USAGE.md deleted file mode 100644 index b1f54d67..00000000 --- a/USAGE.md +++ /dev/null @@ -1,420 +0,0 @@ -# Usage Explanation - -### This guide is still a work in progress - -## Welcome! - -Thank you for your interest in EOCV-Sim :) - -We made this tool in hopes that it will be useful for all FTC teams seeking a way of learning and developing their seasonal OpenCV algorithms in a easy and straightforward way, while also providing some extra tools to improve the experience of developing such algorithms. - -The main purpose of this software is to simulate the package & class structure of OpenFTC's EasyOpenCV and a little bit of the FTC SDK, while also providing OpenCV functionality and a simple GUI. - -By simulating the aforementioned structure, it allows the imports, class names, etc. to be the same as they would if you were using the FTC SDK with EasyOpenCV, allowing you to simply copy paste your vision code onto your Android Studio project once you want to transfer it to a robot.
- -## Table of Contents -- [Workspaces & VS Code](#workspaces--vs-code) -- [IntelliJ Project Structure (with the old installation method)](#intellij-project-structure-with-the-old-installation-method) - - [Creating pipelines](#creating-pipelines-with-the-old-installation-method) -- [Empty Sample Pipeline](#empty-sample-pipeline) -- [Input Sources](#input-sources) - - [Creating an input source](#creating-an-input-source) -- [Telemetry](#telemetry) -- [Variable Tuner](#variable-tuner) - - [Functionality](#tuner-functionality) - - [Configuration](#tuner-configuration) - - [Sample Usage](#sample-usage-of-the-variable-tuner) - -## Workspaces & VS Code - -**This part is applicable with any installation method** - -### Introduction to workspaces - -Workspaces are the new major feature introduced in v3.0.0! - -A workspace basically consists of a folder containing `.java` source files and resource files, which are compiled on the fly by EOCV-Sim. This removes the need of having to use Gradle for running builds, or even allowing to see code changes in real time within a few seconds or even milliseconds! - -Note that a Java Development Kit (JDK) is needed for using this feature, since Java Runtime Environments (JREs) don't come with a compiler packaged, - -The simulator watches for file changes in the background every 800ms, and triggers a build automatically. This can be greately beneficial for writing pipelines, plus the variable tuner that also allows to change variables in real time to reflect changes immediately. The simulator also tries to snapshot the state of the pipeline selected before the build, and try to apply it to the (possibly new if it was compiled on runtime) selected pipeline if the package and class names of the old and the new pipeline match, therefore values tuned with the variable tuner can be kept between builds even if the source code has changed. - -The simulator creates and selects by default a workspace in the user folder, `~/.eocvsim/default_workspace`, which contains a sample GrayscalePipeline that is compiled and added on runtime, but you can change it by doing the following steps: - -1) Go under the "Pipelines" section, click the "Workspace" and finally "Select workspace". Or alternatively, you can also go to Workspace -> Select workspace - - - -2) Select a folder in the file explorer that pops up - -3) Done! The sim should select the folder as a workspace, create a `eocvsim_workspace.json` file if it doesn't exist, start running a build and watch for file changes in the selected folder - -### VS Code - -### The `eocvsim_workspace.json` file - -## IntelliJ project structure (with the old installation method) - -**This part is only applicable if you downloaded EOCV-Sim with the [old method explained in the README](/README.md#altenative-installation-method-intellij-idea)** - -EOCV-Sim uses Gradle starting from v2.0.0, because of this, the project structure is a bit different. For finding the package in which the pipelines have to be placed:
-1) Pop out the parent EOCV-Sim project folder by clicking on the "*>*" arrow -2) Find the TeamCode module (folder) and pop it out just like before -3) Find the src folder and open it -4) Now you will find the *org.firstinspires.ftc.teamcode* package, in which you should place all your pipelines and some sample pipelines are already there.
- -These steps are illustrated in this gif:
- -
- -### Creating pipelines (with the old installation method) - -**This part is also only applicable if you downloaded EOCV-Sim with the [old method explained in the README](/README.md#altenative-installation-method-intellij-idea)** - -As said before, all of the pipeline classes **should be** placed under the *org.firstinspires.ftc.teamcode* package, in the *TeamCode* module. This way, they will be -automatically detected by the simulator and will be selectionable from the GUI. - -
- -*(Also, the simulator already comes by default with some EasyOpenCV samples)*
- -To create a new java class, follow these steps:
-1) In the project files menu, open the TeamCode module -2) Find the *org.firstinspires.ftc.teamcode* package and right click on it -3) On the context menu, click on *New > Java Class* -4) A new menu will appear, type a name and make sure the *Class* option is selected -5) Once you have typed a name, press enter and the class will be created - -Here's a quick gif illustrating these steps:
- -
- -## Empty sample pipeline - -If you want your class to be a pipeline, it **should also** extend the EOCV's OpenCvPipeline abstract class and override the processFrame() method.

-Here's a empty pipeline template, with the SamplePipeline class we created before: - -```java -package org.firstinspires.ftc.teamcode; - -import org.opencv.core.Mat; -import org.openftc.easyopencv.OpenCvPipeline; - -public class SamplePipeline extends OpenCvPipeline { - - @Override - public void init(Mat input) { - /* Executed once, when the pipeline is selected */ - } - - @Override - public Mat processFrame(Mat input) { - /* Executed each frame, the returned mat will be the one displayed */ - /* Processing and detection stuff here */ - return input; // Return the input mat - // (Or a new, processed mat) - } - - @Override - public void onViewportTapped() { - /* - * Executed everytime when the pipeline view is tapped/clicked. - * This is executed from the UI thread, so whatever you do here, - * it must be done it quickly. - */ - } - -} -``` - -### For more detailed information about pipelines, make sure to check out the [EasyOpenCV docs](https://github.com/OpenFTC/EasyOpenCV/blob/master/doc/user_docs/pipelines_overview.md) - -## Input Sources - -To allow multiple ways to test your pipeline, the simulator comes with *Input Sources*, which are the ones in charge of giving your pipeline the input Mats, As of right now, the sim has three types of Input Sources: - -- **Image Source:** - - These will feed your pipeline with a static image loaded in your computer's hard drive. - - To save resources, your pipeline will just run once when you select an image source, but you can optionally resume the pipeline execution by clicking the "Pause" button under the pipeline selector. -- **Camera Source:** - - These will feed your pipeline with a constantly changing video stream from a specified camera plugged in your computer. - - Unlike the image sources, these will not pause the execution of you pipeline by default, but you can click the "Pause" button to pause it at any time. -- **Video Source:** - - These will feed your pipeline with a constantly changing video stream from a file in your hard drive, pause rules are the same as camera sources. - - Most tested video format is *\*.avi*, although it depends on your operating system's support. - -### Creating an Input Source - -1) From your Operating System's file manager, grab a media file such as an image or a video. - -## Telemetry - -There's also an SDK-like Telemetry implementation in the sim. -In 1.1.0 (when it was introduced) you could simply access it from your pipeline since it was an instance variable ```telemetry```. - -But, starting 2.0.0, to make it more alike to an actual EOCV pipeline, you need to implement a public constructor which takes a Telemetry parameter, then creating and setting an instance variable from that constructor: - -```java -package org.firstinspires.ftc.teamcode; - -import org.opencv.core.Mat; -import org.openftc.easyopencv.OpenCvPipeline; - -import org.firstinspires.ftc.robotcore.external.Telemetry; - -public class TelemetryPipeline extends OpenCvPipeline { - - Telemetry telemetry; - - public TelemetryPipeline(Telemetry telemetry) { - this.telemetry = telemetry; - } - - @Override - public Mat processFrame(Mat input) { - telemetry.addData("[Hello]", "World!"); - telemetry.update(); - return input; // Return the input mat - } - -} -``` - -Which then produces the following result:
- -
- -For further information about telemetry, you can check out the [SDK docs on Telemetry](https://ftctechnh.github.io/ftc_app/doc/javadoc/org/firstinspires/ftc/robotcore/external/Telemetry.html), note that not all the methods are implemented for EOCV-Sim - -## Variable Tuner - -From 2.0.0 and on, there's a variable tuner implemented into the simulator, inspired by the one in FTC Dashboard, it allows to edit public, non-final variables from your pipeline in real time seamlessly through Java reflection.
- -This variable tuner can be found at the bottom part of the sim, click on the divider bar to open it:
- -
-
- -This screenshot is from the DefaultPipeline (the one selected when the simulator opens). This variable controls the blur value for the output Mat. You can play with this value to see the tuner functionality.

-If we look into the DefaultPipeline code, we can see that it is simply a **public int** instance variable, not marked as final (alongside with the Telemetry initialization stuff we explained before):
- -
- -The tuner supports a handful of Java types such as most primivites (int, boolean...) and some other types from OpenCV.
-The full list of types currently supported by the tuner on the latest version is:
- - Java: - - int (or Integer) - - float (or Float) - - double (or Double) - - long (or Long) - - boolean (or Boolean) - - String - - Enums - - OpenCV: - - Scalar - - Rect - - Point - -### Tuner functionality - -In the screenshot above, you might have noticed we have three buttons in the field (these buttons appear on field types with at least one textbox/slider). Those were introduced in 2.1.0 to provide extra functionality to the tuner. We have three buttons (options), five parts: - -
- -1) **Text fields/slider toggle** - - Toggles between sliders and textboxes for setting the value to this field -2) **Config** - - Configures various aspects of this tunable field, such as the slider range and picker's color space -3) **Color picker** - - Turns on "color picker" mode, which allows to grab a single pixel from the image and sets it to the selected tunable field - - Sets the color value to the first four textboxes/sliders of this field, if less than four textboxes/sliders are available, it will set the values to the available ones and discard all of the value(s) that can't be copied into the field -4) **Name** - - Displays the name of the variable (declared in the pipeline) -5) **Text field** - - The part in which you can modify the value for this field. Can be toggled to sliders as mentioned before - - Some fields (such as OpenCV Scalars) might have more than one text field - -### Tuner configuration - -When opening the config for a specific field with the aforementioned button (figure #2), you'll see this popup: - -
- -1) **Slider range** - - Sets the **range for the sliders**, defaults to 0-255 since that's the most commonly used, especially for color tuning. - - Negative & positive values allowed, decimal values are allowed only if the field is decimal (such as floats or doubles) -2) **Color space** - - Sets the **color space** for the color picker to return. Defaults to RGB -3) **Apply to all fields...** - - Applies **this configuration** globally or specifically to this field *(see below for further details)* -4) **Config source** - - Displays the source of this config: default global, global, local or specific *(see below for further details)* - -#### Applying tuner configuration - -When using the variable tuner and making configurations, it's sometimes convenient to have some way to store those configurations so that you don't have to reconfig for every field, or every time you select a new pipeline.
- -For this, the sim has a "apply to all" functionality to store common configurations: - -
- -As you can see in the image, when clicking the "apply to all" button, two options appear: - -- **"Globally"** - - Applies the configuration **globally** (to all fields without an "specific" config) - - Note that this doesn't mean that by clicking this option will override all configurations, see below. -- **"Of this type"** (or "specific") - - Applies the configuration to all fields **of the same type** as the current config one. - - (In the case of the example in the screenshot, the blur field in DefaultPipeline, this config will be applied to all *int* fields) - -#### Tuner configuration priority order - -As mentioned before, by applying a "global" configuration, it doesn't mean that it will override the specific configs.
-Rather, there's an specific priority order to determine the configuration that will be given to each tunable field: - -1) **Local** - - This is the one that applies when you modify the configuration values without applying *"globally"* or *"specifically to this type"* - - Simply means that you modified the config without saving it. It will be reset once you select a different pipeline -2) **Type-specific** - - If there's a specific configuration for the type *(such as **int** in the example)*, it will be the one that gets the most priority - - You can define a "type-specific" configuration by clicking on "Applying to all fields..." -> "Of this type" -3) **Global** - - If there's not a type-specific configuration present, the configuration will default to the "global" one - - You can define a "Global" configuration by clicking on "Applying to all fields..." -> "Globally" -5) **Tunable field suggestion** - - If there's not a global configuration, but the current tunable field suggests a *"mode" (sliders or textboxes)*, then that suggestion will be applied - - For example, with OpenCV's Scalars, the tunable field suggest to use sliders since it's more convenient for tuning this type of field -6) **Default global** - - If there's not any configuration or suggestion at all, the field will default to the *"default global"* configuration - - The default config has a slider range from 0 to 255 and a color space of RGB - -### Sample usage of the variable tuner - -Let's say we need to tune a threshold for finding the ring stack in the 2020-2021 "Ultimate Goal" game. For this, we will use the YCrCb color space since it's one of the most used ones in FTC and it behaves better under different lightning conditions. (see [this article](https://learnopencv.com/color-spaces-in-opencv-cpp-python/) for more extended explaination and comparation of different color spaces).
- -We can write a simple pipeline for achieving this, taking advantage of the variable tuner. Here's an example code with detailed comments: - -```java -package org.firstinspires.ftc.teamcode; - -import org.opencv.core.Core; -import org.opencv.core.Mat; -import org.opencv.core.Scalar; -import org.opencv.imgproc.Imgproc; -import org.openftc.easyopencv.OpenCvPipeline; - -public class SimpleThresholdPipeline extends OpenCvPipeline { - - /* - * These are our variables that will be - * modifiable from the variable tuner. - * - * Scalars in OpenCV are generally used to - * represent color. So our values in the - * lower and upper Scalars here represent - * the Y, Cr and Cb values respectively. - * - * YCbCr, like most color spaces, range - * from 0-255, so we default to those - * min and max values here for now, meaning - * that all pixels will be shown. - */ - public Scalar lower = new Scalar(0, 0, 0); - public Scalar upper = new Scalar(255, 255, 255); - - /* - * A good practice when typing EOCV pipelines is - * declaring the Mats you will use here at the top - * of your pipeline, to reuse the same buffers every - * time. This removes the need to call mat.release() - * with every Mat you create on the processFrame method, - * and therefore, reducing the possibility of getting a - * memory leak and causing the app to crash due to an - * "Out of Memory" error. - */ - private Mat ycrcbMat = new Mat(); - private Mat binaryMat = new Mat(); - private Mat maskedInputMat = new Mat(); - - @Override - public Mat processFrame(Mat input) { - /* - * Converts our input mat from RGB to YCrCb. - * EOCV ALWAYS returns RGB mats, so you'd - * always convert from RGB to the color - * space you want to use. - * - * Takes our "input" mat as an input, and outputs - * to a separate Mat buffer "ycrcbMat" - */ - Imgproc.cvtColor(input, ycrcbMat, Imgproc.COLOR_RGB2YCrCb); - - /* - * This is where our thresholding actually happens. - * Takes our "ycrcbMat" as input and outputs a "binary" - * Mat to "binaryMat" of the same size as our input. - * "Discards" all the pixels outside the bounds specified - * by the scalars above (and modifiable with EOCV-Sim's - * live variable tuner.) - * - * Binary meaning that we have either a 0 or 255 value - * for every pixel. - * - * 0 represents our pixels that were outside the bounds - * 255 represents our pixels that are inside the bounds - */ - Core.inRange(ycrcbMat, lower, upper, binaryMat); - - /* - * Release the reusable Mat so that old data doesn't - * affect the next step in the current processing - */ - maskedInputMat.release(); - - /* - * Now, with our binary Mat, we perform a "bitwise and" - * to our input image, meaning that we will perform a mask - * which will include the pixels from our input Mat which - * are "255" in our binary Mat (meaning that they're inside - * the range) and will discard any other pixel outside the - * range (RGB 0, 0, 0. All discarded pixels will be black) - */ - Core.bitwise_and(input, input, maskedInputMat, binaryMat); - - /* - * The Mat returned from this method is the - * one displayed on the viewport. - * - * To visualize our threshold, we'll return - * the "masked input mat" which shows the - * pixel from the input Mat that were inside - * the threshold range. - */ - return maskedInputMat; - } - -} -``` - -And so, when initially selecting this pipeline in the simulator, it's initial state should look something like this:
- -
- -All pixels from the input Mat are visible entirely, this is because we specified a range of 0-255 for all three channels (see the sliders values). Since those values are the minimum (0%) and maximum (100%) for YCrCb respectively, all pixels are able to go through our "threshold".
- -Other thing to note here is that we have sliders instead of textboxes for both Scalars. This is the "default" behavior when using a variable of this type, since sliders are the most optimal option to tune thresholds. This behavior can be overriden by any user configuration, by toggling off the button located at the top left with the sliders icon.

-If you want to permanently change this, go into the field config by clicking on the button with the gear icon, then click "apply to all" and select whether you wanna apply this config to all fields globally, or specifically to the field type (Scalar in this case), as explained before.
- -Anyways, back to the sample. After a bit of playing around with the sliders, it's possible to come up with some decent values which successfully filter out the orange ring stack out of everything else:
- -
- -A problem with the YCrCb color space, especially this year, is that the difference between red and orange is very subtle. So therefore we need to play with the values for a good while until we find some that filters out the red from the goals but displays the ring stack. Or do some other technique alongside thresholding such as [FTCLib's contour ring pipeline](https://github.com/FTCLib/FTCLib/blob/3a43b191b18581a2f741588f9b8ab60c13b7fb6c/core/vision/src/main/java/com/arcrobotics/ftclib/vision/UGContourRingPipeline.kt#L46) with the "horizon" mechanism.
- -Some other nice features can be added to this sample, such as an enum for choosing the color space and Telemetry: - -
- -To keep this explaination simple, you can find the final pipeline [here](https://github.com/serivesmejia/EOCV-Sim/blob/dev/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/SimpleThresholdPipeline.java) with the new demonstrated features, in the TeamCode module, since serves as a good sample alongside other sample classes from EOCV itself. From a86a7bf84a7ed0574ee83048018ab04c4c0e5c2d Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Thu, 23 Sep 2021 15:42:42 -0600 Subject: [PATCH 23/56] Update readme to refer to the gitbook docs instead --- README.md | 104 +++--------------------------------------------------- 1 file changed, 4 insertions(+), 100 deletions(-) diff --git a/README.md b/README.md index c705e7a8..918340a7 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ transfer it onto your robot! -### If you'd like to learn how to use the simulator, you can find a complete usage explaination [here](https://github.com/serivesmejia/EOCV-Sim/blob/master/USAGE.md) +## Learn how to install and use the simulator in the [documentation here](https://deltacv.gitbook.io/eocv-sim/) # Compatibility @@ -25,101 +25,6 @@ Since OpenCV in Java uses a native library, which is platform specific, the simu * MacOS x64 (tested) * Linux x64 (tested for Ubuntu 20.04)
-# Installation - -1) **Download & install the Java Development Kit if you haven't already:**

- JDK 8 is the minimum required one, any JDK above that version will probably work fine.
- You can download it from [the Oracle webpage](https://www.oracle.com/java/technologies/javase-downloads.html), - and here is a [step by step video](https://www.youtube.com/watch?v=IJ-PJbvJBGs) of the installation process
- -## Recommended method - -1) **Make sure you have downloaded a JDK as mentioned above** - -2) **Go to the releases page on this repo and find the latest version ([or click here](https://github.com/deltacv/EOCV-Sim/releases/latest))** - -3) **Download the jar file, named `EOCV-Sim-X.X.X-all.jar`, available at the bottom on the "assets" section** - -4) **Choose and install an IDE/text editor**

- The recommended text editor is VS Code, with the Java Extension Pack. EOCV-Sim provides direct support for it, for creating a "VS Code Workspace" from a template, although it can also be imported into IntelliJ IDEA since it's just a normal Gradle project. - - This installation method provides the benefit of "runtime compiling", which means that the user pipelines are compiled and loaded on the fly and therefore the changes made in code can be reflected immediately, as opposed to the [old IntelliJ IDEA method](#altenative-installation-method-intellij-idea) in which the simulator had to be closed, compiled and then opened again to apply the smallest change made in a pipeline. Plus, VS Code is a lightweight editor which provides Java syntax highlighting and IntelliSense with the Java Extension Pack, making development of pipelines easy with tools like code completion. - - You can download and install VS Code from the [Visual Studio page](https://code.visualstudio.com/). The [Java Extension Pack](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack) can be installed from the [VS Code extension marketplace](https://code.visualstudio.com/docs/introvideos/extend). - - Here's a [tutorial video](https://www.youtube.com/watch?v=KwnavHTOBiA) explaining how to download and install VS Code & the Java Extension Pack - -5) **Running EOCV-Sim**

- For running the sim, simply double click the jar file downloaded from the releases page, or it can also be executed from the command line: - ```python - java -jar "EOCV-Sim-X.X.X-all.jar" - ``` - - When running on Linux (distros such as Ubuntu, Linux Mint, etc) or Unix-like secure operating systems, it might prohibit you to run it by double clicking the file from a file explorer. This can be fixed by giving execute permissions to the jar file with the following command - ```bash - chmod +x EOCV-Sim-X.X.X-all.jar - ``` - -**Now the sim should be running without any issues! If you find any problem feel free to open an issue, and check the [usage explanation](https://github.com/deltacv/EOCV-Sim/blob/master/USAGE.md) for more details about how to use the simulator (and VS Code).** - -## Altenative installation method (IntelliJ IDEA) - -No complicated setup is required for this method either, it's straight up importing the EOCV-Sim project into IntelliJ IDEA: - -\**The downside of this method is that this repo has grown to a considerable amount of space, due to a bloated history, and takes some time to clone, and also builds can be slower depending on your device.* - -1) **Make sure you have downloaded a JDK as mentioned [here](#installation)** - -2) **Download & install IntelliJ IDEA Community IDE if you haven't already:**

- You can download it from the [JetBrains webpage](https://www.jetbrains.com/idea/download/)
- Here is another great [step by step video](https://www.youtube.com/watch?v=E2okEJIbUYs) for IntelliJ installation. - -3) **Clone and import the project:**
- - 1) Open IntelliJ IDEA and in the main screen click on "Get from Version Control"
- -

- Alternatively, if you already had another project opened, go to File > New > Project from Version Control...

- - - 2) Another window will show up for cloning and importing a repository into IntelliJ
- - 1) In the "URL" field, enter: ```https://github.com/deltacv/EOCV-Sim.git```
- 2) The directory can be changed, but it will be automatically filled so it's not necessary. - 3) Make sure the "Version control" is set to "Git".

-
- 4) After that, click on the "Clone" button, located at the bottom right and the cloning process will begin...
-
- 5) After the cloning finishes, the project should automatically import and you'll have something like this:

-
- -### And you're ready to go! Refer to the [usage explanation](https://github.com/deltacv/EOCV-Sim/blob/master/USAGE.md) for further details on how to utilize the simulator.
- -## From the command-line - - 1) Clone EOCV-Sim repo and cd to the cloned folder - - git clone https://github.com/deltacv/EOCV-Sim.git - cd EOCV-Sim - \**Or it can also be manually downloaded as a ZIP file from GitHub*
- - 2) Run EOCV-Sim through gradle: - - gradlew runSim - - \**On some command lines (such as Windows PowerShell and macOS) you might need to execute "./gradlew" instead*
- -#### And that's it! You might need to wait a bit for gradle to download all the dependencies but EOCV-Sim will open eventually. - -## From repl.it - - 1) Click [here](https://repl.it/github/deltacv/EOCV-Sim) to go to repl.it, you might require to create an account if you haven't already. Once you do that, it will automatically create a new project and start cloning the EOCV-Sim repo. - - 2) After the cloning is finished, click on the green "Run" button at the top and EOCV-Sim should start. - - \**Please note that this method is not widely supported and you might run into some issues or lack of some functionality.*
- - ## Adding EOCV-Sim as a dependency ### Gradle @@ -129,7 +34,7 @@ No complicated setup is required for this method either, it's straight up import } dependencies { - implementation 'com.github.deltacv:EOCV-Sim:3.0.0' //add the EOCV-Sim dependency + implementation 'com.github.deltacv:EOCV-Sim:3.2.0' //add the EOCV-Sim dependency } ``` @@ -150,13 +55,12 @@ No complicated setup is required for this method either, it's straight up import com.github.deltacv EOCV-Sim - 3.0.0 + 3.2.0 ``` # Contact -For any quick troubleshooting or help, you can find me on Discord as *serivesmejia#8237* and on the FTC discord server. I'll be happy to assist you in any issue you might have :)

-For bug reporting or feature requesting, use the [issues tab](https://github.com/serivesmejia/EOCV-Sim/issues) in this repository. +For bug reporting or feature requesting, use the [issues tab](https://github.com/deltacv/EOCV-Sim/issues) in this repository. # Change logs From 410fc8959e0269fc0b3cef4611d486002b55bf19 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Thu, 23 Sep 2021 19:01:56 -0600 Subject: [PATCH 24/56] Start working on code gen --- .../github/deltacv/easyvision/EasyVision.kt | 9 +++ .../deltacv/easyvision/codegen/CodeGen.kt | 4 ++ .../deltacv/easyvision/codegen/Scope.kt | 65 +++++++++++++++++++ .../easyvision/node/math/SumIntegerNode.kt | 1 - .../easyvision/node/vision/CvtColorNode.kt | 6 +- imgui.ini | 2 +- 6 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Scope.kt diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt index 0191c499..0458cc69 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt @@ -6,6 +6,8 @@ import imgui.app.Application import imgui.app.Configuration import imgui.flag.ImGuiCond import imgui.flag.ImGuiWindowFlags +import io.github.deltacv.easyvision.codegen.Scope +import io.github.deltacv.easyvision.codegen.Visibility import io.github.deltacv.easyvision.node.NodeEditor import io.github.deltacv.easyvision.node.math.SumIntegerNode import io.github.deltacv.easyvision.node.vision.CvtColorNode @@ -97,5 +99,12 @@ class EasyVision : Application() { } fun main() { + val scope = Scope() + scope.instanceVariable(Visibility.PUBLIC, "int", "number", "0", isFinal = true) + scope.localVariable("Mat", "input", "new Mat()") + scope.methodCall("Imgproc", "cvtColor", "yes", "no", "maybe") + + println(scope.get()) + EasyVision().start() } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt new file mode 100644 index 00000000..1dab0b48 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt @@ -0,0 +1,4 @@ +package io.github.deltacv.easyvision.codegen + +class CodeGen { +} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Scope.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Scope.kt new file mode 100644 index 00000000..968337b7 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Scope.kt @@ -0,0 +1,65 @@ +package io.github.deltacv.easyvision.codegen + +enum class Visibility { + PUBLIC, PRIVATE, PROTECTED +} + +class Scope(private val tabs: Int = 1) { + + private var builder = StringBuilder() + + fun instanceVariable(vis: Visibility, type: String, name: String, + defaultValue: String? = null, + isStatic: Boolean = false, isFinal: Boolean = false) { + newStatement() + + val modifiers = if(isStatic) "static " else "" + + if(isFinal) "final " else " " + + val ending = if(defaultValue != null) "= $defaultValue;" else ";" + + builder.append("${vis.name.lowercase()} $modifiers$type $name $ending") + } + + fun localVariable(type: String, name: String, + defaultValue: String? = null) { + newStatement() + + val ending = if(defaultValue != null) "= $defaultValue;" else ";" + + builder.append("$type $name $ending") + } + + fun methodCall(className: String, methodName: String, vararg parameters: String) { + newStatement() + + builder.append("$className.$methodName(") + + for((i, parameter) in parameters.withIndex()) { + builder.append(parameter) + + if(i < parameters.size - 1) { + builder.append(", ") + } + } + + builder.append(");") + } + + private fun newStatement() { + if(builder.isNotEmpty()) { + builder.appendLine() + } + + insertTabs() + } + + private fun insertTabs() { + repeat(tabs) { + builder.append("\t") + } + } + + fun get() = builder.toString() + +} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt index 06696d2c..a0d21341 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt @@ -8,7 +8,6 @@ class SumIntegerNode : DrawNode("Sum Integer") { override fun onEnable() { + ListAttribute(INPUT, IntAttribute, "Numbers") - + ListAttribute(OUTPUT, IntAttribute, "A") + IntAttribute(OUTPUT,"Result") } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt index c8bdb9f2..65ba2cf0 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt @@ -4,15 +4,15 @@ import io.github.deltacv.easyvision.attribute.misc.EnumAttribute import io.github.deltacv.easyvision.attribute.vision.MatAttribute import io.github.deltacv.easyvision.node.DrawNode -enum class CvtColors { - RGB, BGR, HSV, YCrCb, LAB +enum class Colors { + RGB, BGR, HSV, YCrCb, LAB, GRAY } class CvtColorNode : DrawNode("Convert Color") { override fun onEnable() { + MatAttribute(INPUT, "Input") - + EnumAttribute(INPUT, CvtColors.values(), "Convert To") + + EnumAttribute(INPUT, Colors.values(), "Convert To") + MatAttribute(OUTPUT, "Output") } diff --git a/imgui.ini b/imgui.ini index c276244f..a8087cef 100644 --- a/imgui.ini +++ b/imgui.ini @@ -10,6 +10,6 @@ Collapsed=0 [Window][Editor] Pos=0,0 -Size=1366,711 +Size=1280,708 Collapsed=0 From d04771c59673cc6af7660d70962fe1ae7b9d7b66 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Thu, 23 Sep 2021 19:03:06 -0600 Subject: [PATCH 25/56] Update gitignore --- .gitattributes | 0 .gitignore | 2 +- gradlew | 0 imgui.ini | 15 --------------- 4 files changed, 1 insertion(+), 16 deletions(-) create mode 100644 .gitattributes mode change 100755 => 100644 gradlew delete mode 100644 imgui.ini diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..e69de29b diff --git a/.gitignore b/.gitignore index b1d499f5..1c4b3d23 100644 --- a/.gitignore +++ b/.gitignore @@ -110,4 +110,4 @@ fabric.properties *.DS_Store -**/imgui.ini \ No newline at end of file +imgui.ini \ No newline at end of file diff --git a/gradlew b/gradlew old mode 100755 new mode 100644 diff --git a/imgui.ini b/imgui.ini deleted file mode 100644 index a8087cef..00000000 --- a/imgui.ini +++ /dev/null @@ -1,15 +0,0 @@ -[Window][Debug##Default] -Pos=364,131 -Size=740,474 -Collapsed=0 - -[Window][node editor] -Pos=-42,-22 -Size=1548,708 -Collapsed=0 - -[Window][Editor] -Pos=0,0 -Size=1280,708 -Collapsed=0 - From 0212a02f58830444098cfe3b8176c0d02f270733 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Fri, 24 Sep 2021 13:46:32 -0600 Subject: [PATCH 26/56] Change gradlew to LF --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index e69de29b..1327eff4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -0,0 +1 @@ +gradlew text eol=lf \ No newline at end of file From af3181bbd5f5dee2f51a2a8f4ed0be6010b514a5 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Fri, 24 Sep 2021 13:53:58 -0600 Subject: [PATCH 27/56] Fix gradlew line endings --- gradlew | 344 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 172 insertions(+), 172 deletions(-) diff --git a/gradlew b/gradlew index 518efffc..cccdd3d5 100644 --- a/gradlew +++ b/gradlew @@ -1,172 +1,172 @@ -#!/usr/bin/env sh - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "$@" +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" From 83803d6bf4e6b29f672d96603f5388cea0efa139 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Fri, 24 Sep 2021 19:13:12 -0600 Subject: [PATCH 28/56] Clean code generation --- .../github/deltacv/easyvision/EasyVision.kt | 13 +- .../deltacv/easyvision/codegen/CodeGen.kt | 59 ++++++++- .../deltacv/easyvision/codegen/Scope.kt | 123 ++++++++++++++---- 3 files changed, 166 insertions(+), 29 deletions(-) diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt index 0458cc69..19897767 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt @@ -6,6 +6,7 @@ import imgui.app.Application import imgui.app.Configuration import imgui.flag.ImGuiCond import imgui.flag.ImGuiWindowFlags +import io.github.deltacv.easyvision.codegen.CodeGen import io.github.deltacv.easyvision.codegen.Scope import io.github.deltacv.easyvision.codegen.Visibility import io.github.deltacv.easyvision.node.NodeEditor @@ -99,12 +100,14 @@ class EasyVision : Application() { } fun main() { - val scope = Scope() - scope.instanceVariable(Visibility.PUBLIC, "int", "number", "0", isFinal = true) - scope.localVariable("Mat", "input", "new Mat()") - scope.methodCall("Imgproc", "cvtColor", "yes", "no", "maybe") + val codeGen = CodeGen("TestPipeline") - println(scope.get()) + codeGen.processFrameScope.methodCall( + "Imgproc", "cvtColor", + "input", "mat", "Imgproc.COLOR_RGBA2YCrCb" + ) + + println(codeGen.gen()) EasyVision().start() } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt index 1dab0b48..17d5ab01 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt @@ -1,4 +1,61 @@ package io.github.deltacv.easyvision.codegen -class CodeGen { +enum class Visibility { + PUBLIC, PRIVATE, PROTECTED +} + +class CodeGen(var className: String) { + + val importScope = Scope(0) + val classStartScope = Scope(1) + + val initScope = Scope(1) + val processFrameScope = Scope(1) + val viewportTappedScope = Scope(1) + + init { + importScope.import("org.openftc.easyopencv.OpenCvPipeline") + } + + + fun gen(): String { + val mainScope = Scope(0) + val bodyScope = Scope(1) + + val start = classStartScope.get() + if(start.isNotBlank()) { + bodyScope.scope(classStartScope) + } + + val init = initScope.get() + if(init.isNotBlank()) { + bodyScope.method( + Visibility.PUBLIC, "void", "init", initScope, + Parameter("Mat", "input"), isOverride = true + ) + } + + val process = processFrameScope.get() + if(process.isNotBlank()) { + bodyScope.method( + Visibility.PUBLIC, "Mat", "processFrame", processFrameScope, + Parameter("Mat", "input"), isOverride = true + ) + } + + val viewportTapped = viewportTappedScope.get() + if(viewportTapped.isNotBlank()) { + bodyScope.method( + Visibility.PUBLIC, "Mat", "onViewportTapped", viewportTappedScope, + isOverride = true + ) + } + + mainScope.scope(importScope) + mainScope.newStatement() + mainScope.clazz(Visibility.PUBLIC, className, bodyScope, extends = arrayOf("OpenCvPipeline")) + + return mainScope.get() + } + } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Scope.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Scope.kt index 968337b7..b4bba04e 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Scope.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Scope.kt @@ -1,13 +1,31 @@ package io.github.deltacv.easyvision.codegen -enum class Visibility { - PUBLIC, PRIVATE, PROTECTED -} - -class Scope(private val tabs: Int = 1) { +open class Scope(private val tabsCount: Int = 1) { private var builder = StringBuilder() + private val tabs by lazy { + val builder = StringBuilder() + + repeat(tabsCount) { + builder.append("\t") + } + + builder.toString() + } + + private val imports = mutableListOf() + + fun import(pkg: String) { + if(!imports.contains(pkg)) { + newStatement() + + imports.add(pkg) + + builder.append("import $pkg;") + } + } + fun instanceVariable(vis: Visibility, type: String, name: String, defaultValue: String? = null, isStatic: Boolean = false, isFinal: Boolean = false) { @@ -21,8 +39,10 @@ class Scope(private val tabs: Int = 1) { builder.append("${vis.name.lowercase()} $modifiers$type $name $ending") } - fun localVariable(type: String, name: String, - defaultValue: String? = null) { + fun localVariable( + type: String, name: String, + defaultValue: String? = null + ) { newStatement() val ending = if(defaultValue != null) "= $defaultValue;" else ";" @@ -33,33 +53,90 @@ class Scope(private val tabs: Int = 1) { fun methodCall(className: String, methodName: String, vararg parameters: String) { newStatement() - builder.append("$className.$methodName(") - - for((i, parameter) in parameters.withIndex()) { - builder.append(parameter) + builder.append("$className.$methodName(${parameters.csv()});") + } + + fun method( + vis: Visibility, returnType: String, name: String, body: Scope, + vararg parameters: Parameter, + isStatic: Boolean = false, isFinal: Boolean = false, isOverride: Boolean = true + ) { + newStatement() + builder.appendLine() - if(i < parameters.size - 1) { - builder.append(", ") - } + val static = if(isStatic) "static " else "" + val final = if(isFinal) "final " else "" + + if(isOverride) { + builder.append("$tabs@Override").appendLine() } - builder.append(");") + builder.append(""" + |$tabs${vis.name.lowercase()} $static$final$returnType $name(${parameters.csv()}) { + |$tabs$body + |$tabs} + """.trimMargin()) } - - private fun newStatement() { + + fun clazz(vis: Visibility, name: String, body: Scope, + extends: Array = arrayOf(), implements: Array = arrayOf(), + isStatic: Boolean = false, isFinal: Boolean = false) { + + newStatement() + + val static = if(isStatic) "static " else "" + val final = if(isFinal) "final " else "" + + val e = if(extends.isNotEmpty()) "extends ${extends.csv()} " else "" + val i = if(implements.isNotEmpty()) "implements ${implements.csv()} " else "" + + val endWhitespaceLine = if(!body.get().endsWith("\n")) "\n" else "" + + builder.append(""" + |$tabs${vis.name.lowercase()} $static$final$name $e$i{ + |$tabs$body$endWhitespaceLine + |$tabs} + """.trimMargin()) + } + + fun scope(scope: Scope) { + newStatement() + builder.appendLine().append(scope) + } + + fun newStatement() { if(builder.isNotEmpty()) { builder.appendLine() } - insertTabs() + builder.append(tabs) } - - private fun insertTabs() { - repeat(tabs) { - builder.append("\t") + + fun clear() = builder.clear() + + fun get() = builder.toString() + + override fun toString() = get() + +} + +data class Parameter(val type: String, val name: String) + +fun Array.csv(): String { + val builder = StringBuilder() + + for((i, parameter) in this.withIndex()) { + builder.append(parameter) + + if(i < this.size - 1) { + builder.append(", ") } } - fun get() = builder.toString() + return builder.toString() +} +fun Array.csv(): String { + val stringArray = this.map { "${it.type} ${it.name}" }.toTypedArray() + return stringArray.csv() } \ No newline at end of file From 1f3fc6e3c85ca896266c03e3cc1b92e9402b1be3 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Fri, 24 Sep 2021 20:29:07 -0600 Subject: [PATCH 29/56] Work on code gen and dsl --- .../github/deltacv/easyvision/EasyVision.kt | 11 +++++--- .../deltacv/easyvision/codegen/CodeGen.kt | 18 ++++++++++++- .../deltacv/easyvision/codegen/Scope.kt | 22 ++++++++++++++- .../easyvision/codegen/dsl/CodeGenContext.kt | 27 +++++++++++++++++++ .../easyvision/codegen/dsl/ScopeContext.kt | 19 +++++++++++++ 5 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/CodeGenContext.kt create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/ScopeContext.kt diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt index 19897767..f7e113d8 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt @@ -102,10 +102,13 @@ class EasyVision : Application() { fun main() { val codeGen = CodeGen("TestPipeline") - codeGen.processFrameScope.methodCall( - "Imgproc", "cvtColor", - "input", "mat", "Imgproc.COLOR_RGBA2YCrCb" - ) + codeGen { + enum("Result", "A", "B", "C") + + processFrame { + "Imgproc.cvtColor"("input", "mat", "Imgproc.COLOR_RGBA2GRAY") + } + } println(codeGen.gen()) diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt index 17d5ab01..1c375f79 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt @@ -1,5 +1,7 @@ package io.github.deltacv.easyvision.codegen +import io.github.deltacv.easyvision.codegen.dsl.CodeGenContext + enum class Visibility { PUBLIC, PRIVATE, PROTECTED } @@ -15,9 +17,9 @@ class CodeGen(var className: String) { init { importScope.import("org.openftc.easyopencv.OpenCvPipeline") + importScope.import("org.openftc.easyopencv.OpenCvPipeline") } - fun gen(): String { val mainScope = Scope(0) val bodyScope = Scope(1) @@ -58,4 +60,18 @@ class CodeGen(var className: String) { return mainScope.get() } + private val context = CodeGenContext(this) + + operator fun invoke(block: CodeGenContext.() -> Unit) { + block(context) + } + +} + +fun constructor(type: String, vararg parameters: String) = Constructor(type, parameters) + +data class Constructor(val type: String, val parameters: Array) { + fun value() = parameters[0] + + fun new() = "new $type(${parameters.csv()})" } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Scope.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Scope.kt index b4bba04e..0de6376f 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Scope.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Scope.kt @@ -1,6 +1,8 @@ package io.github.deltacv.easyvision.codegen -open class Scope(private val tabsCount: Int = 1) { +import io.github.deltacv.easyvision.codegen.dsl.ScopeContext + +class Scope(private val tabsCount: Int = 1) { private var builder = StringBuilder() @@ -56,6 +58,12 @@ open class Scope(private val tabsCount: Int = 1) { builder.append("$className.$methodName(${parameters.csv()});") } + fun methodCall(methodName: String, vararg parameters: String) { + newStatement() + + builder.append("$methodName(${parameters.csv()});") + } + fun method( vis: Visibility, returnType: String, name: String, body: Scope, vararg parameters: Parameter, @@ -99,6 +107,12 @@ open class Scope(private val tabsCount: Int = 1) { """.trimMargin()) } + fun enumClass(name: String, vararg values: String) { + newStatement() + + builder.append("enum $name { ${values.csv()} }") + } + fun scope(scope: Scope) { newStatement() builder.appendLine().append(scope) @@ -118,6 +132,12 @@ open class Scope(private val tabsCount: Int = 1) { override fun toString() = get() + private val context = ScopeContext(this) + + operator fun invoke(block: ScopeContext.() -> Unit) { + block(context) + } + } data class Parameter(val type: String, val name: String) diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/CodeGenContext.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/CodeGenContext.kt new file mode 100644 index 00000000..e6187007 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/CodeGenContext.kt @@ -0,0 +1,27 @@ +package io.github.deltacv.easyvision.codegen.dsl + +import io.github.deltacv.easyvision.codegen.CodeGen + +class CodeGenContext(val codeGen: CodeGen) { + + fun import(pkg: String) { + codeGen.importScope.import(pkg) + } + + fun enum(name: String, vararg values: String) { + codeGen.classStartScope.enumClass(name, *values) + } + + fun init(block: ScopeContext.() -> Unit) { + codeGen.initScope(block) + } + + fun processFrame(block: ScopeContext.() -> Unit) { + codeGen.processFrameScope(block) + } + + fun onViewportTapped(block: ScopeContext.() -> Unit) { + codeGen.viewportTappedScope(block) + } + +} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/ScopeContext.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/ScopeContext.kt new file mode 100644 index 00000000..bf769c64 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/ScopeContext.kt @@ -0,0 +1,19 @@ +package io.github.deltacv.easyvision.codegen.dsl + +import io.github.deltacv.easyvision.codegen.Scope + +class ScopeContext(val scope: Scope) { + + operator fun String.invoke(vararg parameters: String) { + scope.methodCall(this, *parameters) + } + + fun local(type: String, value: String) { + + } + + fun constructor() { + + } + +} \ No newline at end of file From 1a2902f2917ab6b225d6569e237b595f6d8f5f04 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Sun, 26 Sep 2021 18:18:59 -0600 Subject: [PATCH 30/56] Work on code gen, cleaner code & making nodes and attribs generate --- .../github/deltacv/easyvision/EasyVision.kt | 32 +++--- .../deltacv/easyvision/attribute/Attribute.kt | 37 +++++++ .../easyvision/attribute/TypedAttribute.kt | 6 +- .../attribute/vision/MatAttribute.kt | 23 ++++ .../deltacv/easyvision/codegen/CodeGen.kt | 25 +++-- .../easyvision/codegen/CodeGenSession.kt | 5 + .../github/deltacv/easyvision/codegen/Csv.kt | 25 +++++ .../deltacv/easyvision/codegen/Scope.kt | 103 ++++++++++-------- .../deltacv/easyvision/codegen/Value.kt | 20 ++++ .../easyvision/codegen/dsl/CodeGenContext.kt | 26 ++++- .../easyvision/codegen/dsl/ScopeContext.kt | 18 ++- .../easyvision/codegen/type/GenType.kt | 11 ++ .../easyvision/codegen/vision/MatRecycle.kt | 4 + .../easyvision/exception/GenException.kt | 8 ++ .../io/github/deltacv/easyvision/node/Node.kt | 12 ++ .../easyvision/node/vision/CvtColorNode.kt | 21 +++- .../easyvision/node/vision/MatNodes.kt | 21 +++- 17 files changed, 310 insertions(+), 87 deletions(-) create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGenSession.kt create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Csv.kt create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Value.kt create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/type/GenType.kt create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/vision/MatRecycle.kt create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/exception/GenException.kt diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt index f7e113d8..15ac7115 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt @@ -5,12 +5,12 @@ import imgui.ImVec2 import imgui.app.Application import imgui.app.Configuration import imgui.flag.ImGuiCond +import imgui.flag.ImGuiKey import imgui.flag.ImGuiWindowFlags -import io.github.deltacv.easyvision.codegen.CodeGen -import io.github.deltacv.easyvision.codegen.Scope -import io.github.deltacv.easyvision.codegen.Visibility +import io.github.deltacv.easyvision.codegen.* import io.github.deltacv.easyvision.node.NodeEditor import io.github.deltacv.easyvision.node.math.SumIntegerNode +import io.github.deltacv.easyvision.node.vision.Colors import io.github.deltacv.easyvision.node.vision.CvtColorNode import io.github.deltacv.easyvision.node.vision.InputMatNode import io.github.deltacv.easyvision.node.vision.OutputMatNode @@ -42,11 +42,14 @@ class EasyVision : Application() { val editor = NodeEditor(this) + val inputNode = InputMatNode() + val outputNode = OutputMatNode() + fun start() { editor.init() - InputMatNode().enable() - OutputMatNode().enable() + inputNode.enable() + outputNode.enable() SumIntegerNode().enable() SumIntegerNode().enable() @@ -85,6 +88,13 @@ class EasyVision : Application() { PopupBuilder.draw() isDeleteReleased = false + + if(ImGui.isKeyReleased(ImGuiKey.Z)) { + val codeGen = CodeGen("TestPipeline") + outputNode.genCode(codeGen) + + println(codeGen) + } } var isDeleteReleased = false @@ -100,17 +110,5 @@ class EasyVision : Application() { } fun main() { - val codeGen = CodeGen("TestPipeline") - - codeGen { - enum("Result", "A", "B", "C") - - processFrame { - "Imgproc.cvtColor"("input", "mat", "Imgproc.COLOR_RGBA2GRAY") - } - } - - println(codeGen.gen()) - EasyVision().start() } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt index 82635ea6..bd1d8235 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt @@ -1,6 +1,9 @@ package io.github.deltacv.easyvision.attribute import imgui.extension.imnodes.ImNodes +import io.github.deltacv.easyvision.codegen.CodeGen +import io.github.deltacv.easyvision.codegen.type.GenValue +import io.github.deltacv.easyvision.exception.AttributeGenException import io.github.deltacv.easyvision.id.DrawableIdElement import io.github.deltacv.easyvision.node.Link import io.github.deltacv.easyvision.node.Node @@ -16,9 +19,15 @@ abstract class Attribute : DrawableIdElement { lateinit var parentNode: Node internal set + var relativeIndex = 0 + internal set + val links = mutableListOf() val hasLink get() = links.isNotEmpty() + val isInput = mode == AttributeMode.INPUT + val isOutput = !isInput + abstract fun drawAttribute() override fun draw() { @@ -50,6 +59,34 @@ abstract class Attribute : DrawableIdElement { Node.attributes[id] = this } + fun linkedAttribute(): Attribute? { + if(!isInput) { + raise("Output attributes might have more than one link, so linkedAttribute() is not allowed") + } + + if(!hasLink) { + return null + } + + val link = links[0] + + return if(link.aAttrib == this) { + link.aAttrib + } else link.bAttrib + } + + fun raise(message: String): Nothing = throw AttributeGenException(this, message) + + fun raiseAssert(condition: Boolean, message: String) { + if(condition) { + raise(message) + } + } + abstract fun acceptLink(other: Attribute): Boolean + abstract fun value(codeGen: CodeGen): GenValue + + protected fun getOutputValue(codeGen: CodeGen) = parentNode.getOutputValueOf(codeGen, relativeIndex) + } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt index 6afd8502..07f3406e 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt @@ -1,6 +1,10 @@ package io.github.deltacv.easyvision.attribute import imgui.ImGui +import io.github.deltacv.easyvision.codegen.CodeGen +import io.github.deltacv.easyvision.codegen.type.GenValue +import io.github.deltacv.easyvision.exception.AttributeGenException +import io.github.deltacv.easyvision.node.Link interface Type { val name: String @@ -18,7 +22,7 @@ abstract class TypedAttribute(var type: Type) : Attribute() { var drawDescriptiveText = true var drawType = true - protected val finalVarName by lazy { + private val finalVarName by lazy { variableName ?: if (mode == AttributeMode.INPUT) "Input" else "Output" } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/MatAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/MatAttribute.kt index 560010fb..9e97d6bf 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/MatAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/MatAttribute.kt @@ -3,6 +3,8 @@ package io.github.deltacv.easyvision.attribute.vision import io.github.deltacv.easyvision.attribute.TypedAttribute import io.github.deltacv.easyvision.attribute.AttributeMode import io.github.deltacv.easyvision.attribute.Type +import io.github.deltacv.easyvision.codegen.CodeGen +import io.github.deltacv.easyvision.codegen.type.GenValue class MatAttribute( override val mode: AttributeMode, @@ -15,4 +17,25 @@ class MatAttribute( override fun new(mode: AttributeMode, variableName: String) = MatAttribute(mode, variableName) } + override fun value(codeGen: CodeGen): GenValue.Mat { + if(isInput) { + val linkedAttrib = linkedAttribute() + + raiseAssert( + linkedAttrib != null, + "Mat attribute must have another attribute attached" + ) + + val value = linkedAttrib!!.value(codeGen) + raiseAssert(value is GenValue.Mat, "Attribute attached is not a Mat") + + return value as GenValue.Mat + } else { + val value = getOutputValue(codeGen) + raiseAssert(value is GenValue.Mat, "Value returned from the node is not a Mat") + + return value as GenValue.Mat + } + } + } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt index 1c375f79..375cb64f 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt @@ -10,14 +10,14 @@ class CodeGen(var className: String) { val importScope = Scope(0) val classStartScope = Scope(1) + val classEndScope = Scope(1) - val initScope = Scope(1) - val processFrameScope = Scope(1) - val viewportTappedScope = Scope(1) + val initScope = Scope(2) + val processFrameScope = Scope(2) + val viewportTappedScope = Scope(2) init { importScope.import("org.openftc.easyopencv.OpenCvPipeline") - importScope.import("org.openftc.easyopencv.OpenCvPipeline") } fun gen(): String { @@ -27,6 +27,7 @@ class CodeGen(var className: String) { val start = classStartScope.get() if(start.isNotBlank()) { bodyScope.scope(classStartScope) + bodyScope.newStatement() } val init = initScope.get() @@ -35,6 +36,7 @@ class CodeGen(var className: String) { Visibility.PUBLIC, "void", "init", initScope, Parameter("Mat", "input"), isOverride = true ) + bodyScope.newStatement() } val process = processFrameScope.get() @@ -47,12 +49,19 @@ class CodeGen(var className: String) { val viewportTapped = viewportTappedScope.get() if(viewportTapped.isNotBlank()) { + bodyScope.newStatement() + bodyScope.method( Visibility.PUBLIC, "Mat", "onViewportTapped", viewportTappedScope, isOverride = true ) } + val end = classEndScope.get() + if(end.isNotBlank()) { + bodyScope.scope(classEndScope) + } + mainScope.scope(importScope) mainScope.newStatement() mainScope.clazz(Visibility.PUBLIC, className, bodyScope, extends = arrayOf("OpenCvPipeline")) @@ -66,12 +75,4 @@ class CodeGen(var className: String) { block(context) } -} - -fun constructor(type: String, vararg parameters: String) = Constructor(type, parameters) - -data class Constructor(val type: String, val parameters: Array) { - fun value() = parameters[0] - - fun new() = "new $type(${parameters.csv()})" } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGenSession.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGenSession.kt new file mode 100644 index 00000000..63142a8d --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGenSession.kt @@ -0,0 +1,5 @@ +package io.github.deltacv.easyvision.codegen + +class CodeGenSession { +} + diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Csv.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Csv.kt new file mode 100644 index 00000000..5b2fff25 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Csv.kt @@ -0,0 +1,25 @@ +package io.github.deltacv.easyvision.codegen + +fun Array.csv(): String { + val builder = StringBuilder() + + for((i, parameter) in this.withIndex()) { + builder.append(parameter) + + if(i < this.size - 1) { + builder.append(", ") + } + } + + return builder.toString() +} + +fun Array.csv(): String { + val stringArray = this.map { "${it.type} ${it.name}" }.toTypedArray() + return stringArray.csv() +} + +fun Array.csv(): String { + val stringArray = this.map { it.value!! }.toTypedArray() + return stringArray.csv() +} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Scope.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Scope.kt index 0de6376f..9c95c5bd 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Scope.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Scope.kt @@ -28,49 +28,57 @@ class Scope(private val tabsCount: Int = 1) { } } - fun instanceVariable(vis: Visibility, type: String, name: String, - defaultValue: String? = null, + fun instanceVariable(vis: Visibility, name: String, + variable: Value, isStatic: Boolean = false, isFinal: Boolean = false) { newStatement() - val modifiers = if(isStatic) "static " else "" + - if(isFinal) "final " else " " + val modifiers = if(isStatic) " static" else "" + + if(isFinal) " final" else "" - val ending = if(defaultValue != null) "= $defaultValue;" else ";" + val ending = if(variable.value != null) "= ${variable.value};" else ";" - builder.append("${vis.name.lowercase()} $modifiers$type $name $ending") + builder.append("$tabs${vis.name.lowercase()}$modifiers ${variable.type} $name $ending") } - fun localVariable( - type: String, name: String, - defaultValue: String? = null - ) { + fun localVariable(name: String, variable: Value) { newStatement() + + val ending = if(variable.value != null) "= ${variable.value};" else ";" - val ending = if(defaultValue != null) "= $defaultValue;" else ";" - - builder.append("$type $name $ending") + builder.append("$tabs${variable.type} $name $ending") + } + + fun variableSet(name: String, v: Value) { + newStatement() + + builder.append("$tabs$name = ${v.value!!};") + } + + fun instanceVariableSet(name: String, v: Value) { + newStatement() + + builder.append("${tabs}this.$name = ${v.value!!};") } - fun methodCall(className: String, methodName: String, vararg parameters: String) { + fun methodCall(className: String, methodName: String, vararg parameters: Value) { newStatement() - builder.append("$className.$methodName(${parameters.csv()});") + builder.append("$tabs$className.$methodName(${parameters.csv()});") } - fun methodCall(methodName: String, vararg parameters: String) { + fun methodCall(methodName: String, vararg parameters: Value) { newStatement() - builder.append("$methodName(${parameters.csv()});") + builder.append("$tabs$methodName(${parameters.csv()});") } fun method( vis: Visibility, returnType: String, name: String, body: Scope, vararg parameters: Parameter, - isStatic: Boolean = false, isFinal: Boolean = false, isOverride: Boolean = true + isStatic: Boolean = false, isFinal: Boolean = false, isOverride: Boolean = false ) { - newStatement() - builder.appendLine() + newLineIfNotBlank() val static = if(isStatic) "static " else "" val final = if(isFinal) "final " else "" @@ -81,11 +89,21 @@ class Scope(private val tabsCount: Int = 1) { builder.append(""" |$tabs${vis.name.lowercase()} $static$final$returnType $name(${parameters.csv()}) { - |$tabs$body + |$body |$tabs} """.trimMargin()) } + fun returnMethod(value: Value? = null) { + newStatement() + + if(value != null) { + builder.append("${tabs}return ${value.value!!};") + } else { + builder.append("${tabs}return;") + } + } + fun clazz(vis: Visibility, name: String, body: Scope, extends: Array = arrayOf(), implements: Array = arrayOf(), isStatic: Boolean = false, isFinal: Boolean = false) { @@ -102,7 +120,7 @@ class Scope(private val tabsCount: Int = 1) { builder.append(""" |$tabs${vis.name.lowercase()} $static$final$name $e$i{ - |$tabs$body$endWhitespaceLine + |$body$endWhitespaceLine |$tabs} """.trimMargin()) } @@ -110,20 +128,30 @@ class Scope(private val tabsCount: Int = 1) { fun enumClass(name: String, vararg values: String) { newStatement() - builder.append("enum $name { ${values.csv()} }") + builder.append("${tabs}enum $name { ${values.csv()} }") } fun scope(scope: Scope) { - newStatement() - builder.appendLine().append(scope) + newLineIfNotBlank() + builder.append(scope) } fun newStatement() { if(builder.isNotEmpty()) { builder.appendLine() } + } + + fun newLineIfNotBlank() { + val str = get() - builder.append(tabs) + println(str) + + if(!str.endsWith("\n\n") && str.endsWith("\n")) { + builder.appendLine() + } else if(!str.endsWith("\n\n")) { + builder.append("\n") + } } fun clear() = builder.clear() @@ -132,7 +160,7 @@ class Scope(private val tabsCount: Int = 1) { override fun toString() = get() - private val context = ScopeContext(this) + internal val context = ScopeContext(this) operator fun invoke(block: ScopeContext.() -> Unit) { block(context) @@ -140,23 +168,4 @@ class Scope(private val tabsCount: Int = 1) { } -data class Parameter(val type: String, val name: String) - -fun Array.csv(): String { - val builder = StringBuilder() - - for((i, parameter) in this.withIndex()) { - builder.append(parameter) - - if(i < this.size - 1) { - builder.append(", ") - } - } - - return builder.toString() -} - -fun Array.csv(): String { - val stringArray = this.map { "${it.type} ${it.name}" }.toTypedArray() - return stringArray.csv() -} \ No newline at end of file +data class Parameter(val type: String, val name: String) \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Value.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Value.kt new file mode 100644 index 00000000..ed42ef03 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Value.kt @@ -0,0 +1,20 @@ +package io.github.deltacv.easyvision.codegen + +import io.github.deltacv.easyvision.node.vision.Colors + +fun new(type: String, vararg parameters: String) = Value(type, "new $type(${parameters.csv()})") + +fun value(type: String, value: String) = Value(type, value) + +fun callValue(methodName: String, returnType: String, vararg parameters: Value) = + Value(returnType, "$methodName(${parameters.csv()})") + +fun enumValue(type: String, constantName: String) = Value(type, "$type.$constantName") + +fun cvtColorValue(a: Colors, b: Colors) = Value("int", "Imgproc.COLOR_${a.name}2${b.name}") + +fun variable(type: String) = Value(type, null) + +val String.v get() = Value("", this) + +data class Value(val type: String, val value: String?) \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/CodeGenContext.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/CodeGenContext.kt index e6187007..c6b41bba 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/CodeGenContext.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/CodeGenContext.kt @@ -1,6 +1,6 @@ package io.github.deltacv.easyvision.codegen.dsl -import io.github.deltacv.easyvision.codegen.CodeGen +import io.github.deltacv.easyvision.codegen.* class CodeGenContext(val codeGen: CodeGen) { @@ -24,4 +24,28 @@ class CodeGenContext(val codeGen: CodeGen) { codeGen.viewportTappedScope(block) } + fun public(name: String, v: Value) = + codeGen.classStartScope.instanceVariable(Visibility.PUBLIC, name, v) + + fun private(name: String, v: Value) = + codeGen.classStartScope.instanceVariable(Visibility.PRIVATE, name, v) + + fun protected(name: String, v: Value) = + codeGen.classStartScope.instanceVariable(Visibility.PROTECTED, name, v) + + operator fun String.invoke( + vis: Visibility, returnType: String, + vararg parameters: Parameter, + isStatic: Boolean = false, isFinal: Boolean = false, isOverride: Boolean = true, + scopeBlock: ScopeContext.() -> Unit + ) { + val s = Scope(2) + scopeBlock(s.context) + + codeGen.classEndScope.method( + vis, returnType, this, s, *parameters, + isStatic = isStatic, isFinal = isFinal, isOverride = isOverride + ) + } + } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/ScopeContext.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/ScopeContext.kt index bf769c64..29b53608 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/ScopeContext.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/ScopeContext.kt @@ -1,19 +1,27 @@ package io.github.deltacv.easyvision.codegen.dsl import io.github.deltacv.easyvision.codegen.Scope +import io.github.deltacv.easyvision.codegen.Value +import io.github.deltacv.easyvision.codegen.Visibility class ScopeContext(val scope: Scope) { - operator fun String.invoke(vararg parameters: String) { + operator fun String.invoke(vararg parameters: Value) { scope.methodCall(this, *parameters) } - fun local(type: String, value: String) { + infix fun String.value(v: Value) = + scope.instanceVariable(Visibility.PUBLIC, this, v) - } + fun String.local(name: String, v: Value) = + scope.localVariable(name, v) - fun constructor() { + infix fun String.set(v: Value) = + scope.variableSet(this, v) - } + infix fun String.instanceSet(v: Value) = + scope.instanceVariableSet(this, v) + + fun returnMethod(value: Value? = null) = scope.returnMethod(value) } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/type/GenType.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/type/GenType.kt new file mode 100644 index 00000000..182baec6 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/type/GenType.kt @@ -0,0 +1,11 @@ +package io.github.deltacv.easyvision.codegen.type + +import io.github.deltacv.easyvision.codegen.Value + +sealed class GenValue { + + data class Mat(val value: Value) : GenValue() + + object None : GenValue() + +} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/vision/MatRecycle.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/vision/MatRecycle.kt new file mode 100644 index 00000000..3e22642b --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/vision/MatRecycle.kt @@ -0,0 +1,4 @@ +package io.github.deltacv.easyvision.codegen.vision + +class MatRecycle { +} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/exception/GenException.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/exception/GenException.kt new file mode 100644 index 00000000..302a032d --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/exception/GenException.kt @@ -0,0 +1,8 @@ +package io.github.deltacv.easyvision.exception + +import io.github.deltacv.easyvision.attribute.Attribute +import io.github.deltacv.easyvision.node.Node + +class NodeGenException(val node: Node, override val message: String) : RuntimeException(message) + +class AttributeGenException(val attribute: Attribute, override val message: String) : RuntimeException(message) \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt index 531238e8..8e94fbea 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt @@ -5,6 +5,9 @@ import io.github.deltacv.easyvision.id.DrawableIdElement import io.github.deltacv.easyvision.id.IdElementContainer import io.github.deltacv.easyvision.attribute.Attribute import io.github.deltacv.easyvision.attribute.AttributeMode +import io.github.deltacv.easyvision.codegen.CodeGen +import io.github.deltacv.easyvision.codegen.CodeGenSession +import io.github.deltacv.easyvision.codegen.type.GenValue interface Type { val name: String @@ -59,11 +62,20 @@ abstract class Node(private var allowDelete: Boolean = true) : DrawableIdElement fun addAttribute(attribute: Attribute) { attribute.parentNode = this + attribute.relativeIndex = attribs.size attribs.add(attribute) } operator fun Attribute.unaryPlus() = addAttribute(this) + abstract fun genCode(codeGen: CodeGen): CodeGenSession + + /** + * The index corresponds to the order the attributes were added + * starting from 0 of course + */ + abstract fun getOutputValueOf(codeGen: CodeGen, index: Int): GenValue + companion object { val nodes = IdElementContainer() val attributes = IdElementContainer() diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt index 65ba2cf0..5fbddb1c 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt @@ -2,6 +2,8 @@ package io.github.deltacv.easyvision.node.vision import io.github.deltacv.easyvision.attribute.misc.EnumAttribute import io.github.deltacv.easyvision.attribute.vision.MatAttribute +import io.github.deltacv.easyvision.codegen.CodeGen +import io.github.deltacv.easyvision.codegen.type.GenValue import io.github.deltacv.easyvision.node.DrawNode enum class Colors { @@ -10,11 +12,24 @@ enum class Colors { class CvtColorNode : DrawNode("Convert Color") { + val input = MatAttribute(INPUT, "Input") + val output = MatAttribute(OUTPUT, "Output") + + val convertTo = EnumAttribute(INPUT, Colors.values(), "Convert To") + override fun onEnable() { - + MatAttribute(INPUT, "Input") - + EnumAttribute(INPUT, Colors.values(), "Convert To") + + input + + convertTo + + + output + } + + override fun genCode(codeGen: CodeGen) { + TODO("Not yet implemented") + } - + MatAttribute(OUTPUT, "Output") + override fun getOutputValueOf(codeGen: CodeGen, index: Int): GenValue { + TODO("Not yet implemented") } } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt index d39c6a1a..d1a692d4 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt @@ -2,15 +2,34 @@ package io.github.deltacv.easyvision.node.vision import io.github.deltacv.easyvision.node.DrawNode import io.github.deltacv.easyvision.attribute.vision.MatAttribute +import io.github.deltacv.easyvision.codegen.CodeGen +import io.github.deltacv.easyvision.codegen.type.GenValue +import io.github.deltacv.easyvision.codegen.v class InputMatNode : DrawNode("Pipeline Input", allowDelete = false) { override fun onEnable() { + MatAttribute(OUTPUT, "Input") } + + override fun genCode(codeGen: CodeGen) { + } + + override fun getOutputValueOf(codeGen: CodeGen, index: Int) = GenValue.Mat("input".v) } class OutputMatNode : DrawNode("Pipeline Output", allowDelete = false) { + + val input = MatAttribute(INPUT, "Output") + override fun onEnable() { - + MatAttribute(INPUT, "Output") + + input + } + + override fun genCode(codeGen: CodeGen) = codeGen { + processFrame { + returnMethod(input.value(codeGen).value) // start code gen! + } } + + override fun getOutputValueOf(codeGen: CodeGen, index: Int) = GenValue.None } \ No newline at end of file From 8da8579d86e7e859fa5c719c2fe1ab8ee51b253e Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Sun, 26 Sep 2021 19:26:05 -0600 Subject: [PATCH 31/56] fteVAlmost finished basic code gen --- .../deltacv/easyvision/attribute/Attribute.kt | 7 ++-- .../attribute/misc/EnumAttribute.kt | 6 ++++ .../deltacv/easyvision/codegen/CodeGen.kt | 9 ++--- .../easyvision/codegen/CodeGenSession.kt | 4 +-- .../easyvision/codegen/type/GenType.kt | 5 ++- .../deltacv/easyvision/node/DrawNode.kt | 6 +++- .../io/github/deltacv/easyvision/node/Node.kt | 36 +++++++++++++++---- .../easyvision/node/vision/CvtColorNode.kt | 30 +++++++++++++--- .../easyvision/node/vision/MatNodes.kt | 13 +++++-- 9 files changed, 89 insertions(+), 27 deletions(-) diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt index bd1d8235..f1dfdcb3 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt @@ -16,10 +16,7 @@ abstract class Attribute : DrawableIdElement { override val id by Node.attributes.nextId { this } - lateinit var parentNode: Node - internal set - - var relativeIndex = 0 + lateinit var parentNode: Node<*> internal set val links = mutableListOf() @@ -87,6 +84,6 @@ abstract class Attribute : DrawableIdElement { abstract fun value(codeGen: CodeGen): GenValue - protected fun getOutputValue(codeGen: CodeGen) = parentNode.getOutputValueOf(codeGen, relativeIndex) + protected fun getOutputValue(codeGen: CodeGen) = parentNode.getOutputValueOf(codeGen, this) } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/EnumAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/EnumAttribute.kt index d69d7e6e..ea571547 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/EnumAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/EnumAttribute.kt @@ -6,6 +6,8 @@ import io.github.deltacv.easyvision.attribute.Attribute import io.github.deltacv.easyvision.attribute.AttributeMode import io.github.deltacv.easyvision.attribute.Type import io.github.deltacv.easyvision.attribute.TypedAttribute +import io.github.deltacv.easyvision.codegen.CodeGen +import io.github.deltacv.easyvision.codegen.type.GenValue class EnumAttribute>( override val mode: AttributeMode, @@ -34,4 +36,8 @@ class EnumAttribute>( override fun acceptLink(other: Attribute) = other is EnumAttribute<*> && values[0]::class == other.values[0]::class + override fun value(codeGen: CodeGen): GenValue { + TODO("Not yet implemented") + } + } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt index 375cb64f..f05f78ac 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt @@ -1,6 +1,7 @@ package io.github.deltacv.easyvision.codegen import io.github.deltacv.easyvision.codegen.dsl.CodeGenContext +import io.github.deltacv.easyvision.node.Node enum class Visibility { PUBLIC, PRIVATE, PROTECTED @@ -10,12 +11,14 @@ class CodeGen(var className: String) { val importScope = Scope(0) val classStartScope = Scope(1) - val classEndScope = Scope(1) + val classEndScope = Scope(1) val initScope = Scope(2) val processFrameScope = Scope(2) val viewportTappedScope = Scope(2) + val sessions = mutableMapOf, CodeGenSession>() + init { importScope.import("org.openftc.easyopencv.OpenCvPipeline") } @@ -71,8 +74,6 @@ class CodeGen(var className: String) { private val context = CodeGenContext(this) - operator fun invoke(block: CodeGenContext.() -> Unit) { - block(context) - } + operator fun invoke(block: CodeGenContext.() -> T) = block(context) } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGenSession.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGenSession.kt index 63142a8d..be025300 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGenSession.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGenSession.kt @@ -1,5 +1,3 @@ package io.github.deltacv.easyvision.codegen -class CodeGenSession { -} - +open class CodeGenSession diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/type/GenType.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/type/GenType.kt index 182baec6..25f9cc7a 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/type/GenType.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/type/GenType.kt @@ -1,10 +1,13 @@ package io.github.deltacv.easyvision.codegen.type import io.github.deltacv.easyvision.codegen.Value +import io.github.deltacv.easyvision.node.vision.Colors sealed class GenValue { - data class Mat(val value: Value) : GenValue() + data class Mat(val value: Value, val color: Colors) : GenValue() + + data class Enum>(val value: E) : GenValue() object None : GenValue() diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/DrawNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/DrawNode.kt index ccd94648..9f2e12ba 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/DrawNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/DrawNode.kt @@ -2,8 +2,12 @@ package io.github.deltacv.easyvision.node import imgui.ImGui import imgui.extension.imnodes.ImNodes +import io.github.deltacv.easyvision.codegen.CodeGenSession -abstract class DrawNode(var title: String? = null, allowDelete: Boolean = true) : Node(allowDelete) { +abstract class DrawNode( + var title: String? = null, + allowDelete: Boolean = true +) : Node(allowDelete) { override fun draw() { ImNodes.beginNode(id) diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt index 8e94fbea..ea0f46b2 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt @@ -8,18 +8,24 @@ import io.github.deltacv.easyvision.attribute.AttributeMode import io.github.deltacv.easyvision.codegen.CodeGen import io.github.deltacv.easyvision.codegen.CodeGenSession import io.github.deltacv.easyvision.codegen.type.GenValue +import io.github.deltacv.easyvision.exception.NodeGenException interface Type { val name: String } -abstract class Node(private var allowDelete: Boolean = true) : DrawableIdElement { +abstract class Node( + private var allowDelete: Boolean = true +) : DrawableIdElement { override val id by nodes.nextId { this } private val attribs = mutableListOf() // internal mutable list val nodeAttributes = attribs as List // public read-only + var genSession: S? = null + private set + protected fun drawAttributes() { for((i, attribute) in nodeAttributes.withIndex()) { attribute.draw() @@ -62,28 +68,46 @@ abstract class Node(private var allowDelete: Boolean = true) : DrawableIdElement fun addAttribute(attribute: Attribute) { attribute.parentNode = this - attribute.relativeIndex = attribs.size attribs.add(attribute) } operator fun Attribute.unaryPlus() = addAttribute(this) - abstract fun genCode(codeGen: CodeGen): CodeGenSession + abstract fun genCode(codeGen: CodeGen): S /** * The index corresponds to the order the attributes were added * starting from 0 of course */ - abstract fun getOutputValueOf(codeGen: CodeGen, index: Int): GenValue + abstract fun getOutputValueOf(codeGen: CodeGen, attrib: Attribute): GenValue + + @Suppress("UNCHECKED_CAST") + fun genCodeIfNecessary(codeGen: CodeGen) { + val session = codeGen.sessions[this] + + if(session == null) { + codeGen.sessions[this] = genCode(codeGen) + } else { + genSession = session as S + } + } + + fun raise(message: String): Nothing = throw NodeGenException(this, message) + + fun raiseAssert(condition: Boolean, message: String) { + if(condition) { + raise(message) + } + } companion object { - val nodes = IdElementContainer() + val nodes = IdElementContainer>() val attributes = IdElementContainer() @JvmStatic protected val INPUT = AttributeMode.INPUT @JvmStatic protected val OUTPUT = AttributeMode.OUTPUT - fun checkRecursion(from: Node, to: Node): Boolean { + fun checkRecursion(from: Node<*>, to: Node<*>): Boolean { val linksBetween = Link.getLinksBetween(from, to) var hasOutputToInput = false diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt index 5fbddb1c..bcb27c51 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt @@ -1,8 +1,10 @@ package io.github.deltacv.easyvision.node.vision +import io.github.deltacv.easyvision.attribute.Attribute import io.github.deltacv.easyvision.attribute.misc.EnumAttribute import io.github.deltacv.easyvision.attribute.vision.MatAttribute import io.github.deltacv.easyvision.codegen.CodeGen +import io.github.deltacv.easyvision.codegen.CodeGenSession import io.github.deltacv.easyvision.codegen.type.GenValue import io.github.deltacv.easyvision.node.DrawNode @@ -10,7 +12,7 @@ enum class Colors { RGB, BGR, HSV, YCrCb, LAB, GRAY } -class CvtColorNode : DrawNode("Convert Color") { +class CvtColorNode : DrawNode("Convert Color") { val input = MatAttribute(INPUT, "Input") val output = MatAttribute(OUTPUT, "Output") @@ -24,12 +26,30 @@ class CvtColorNode : DrawNode("Convert Color") { + output } - override fun genCode(codeGen: CodeGen) { - TODO("Not yet implemented") + override fun genCode(codeGen: CodeGen) = codeGen { + val session = Session() + + val inputMat = input.value(codeGen) + + processFrame { + + } + + session + } + + override fun getOutputValueOf(codeGen: CodeGen, attrib: Attribute): GenValue { + genCodeIfNecessary(codeGen) + + if(attrib == output) { + return genSession!!.outputMatValue + } + + raise("Attribute $attrib is not an output of this node or not handled by this") } - override fun getOutputValueOf(codeGen: CodeGen, index: Int): GenValue { - TODO("Not yet implemented") + class Session : CodeGenSession() { + lateinit var outputMatValue: GenValue.Mat } } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt index d1a692d4..30a62d01 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt @@ -3,18 +3,23 @@ package io.github.deltacv.easyvision.node.vision import io.github.deltacv.easyvision.node.DrawNode import io.github.deltacv.easyvision.attribute.vision.MatAttribute import io.github.deltacv.easyvision.codegen.CodeGen +import io.github.deltacv.easyvision.codegen.CodeGenSession import io.github.deltacv.easyvision.codegen.type.GenValue import io.github.deltacv.easyvision.codegen.v class InputMatNode : DrawNode("Pipeline Input", allowDelete = false) { + + override var genSession: CodeGenSession? = null + override fun onEnable() { + MatAttribute(OUTPUT, "Input") } - override fun genCode(codeGen: CodeGen) { + override fun genCode(codeGen: CodeGen): CodeGenSession { + raise("Input Mat node cannot generate code") } - override fun getOutputValueOf(codeGen: CodeGen, index: Int) = GenValue.Mat("input".v) + override fun getOutputValueOf(codeGen: CodeGen, index: Int) = GenValue.Mat("input".v, Colors.RGB) } class OutputMatNode : DrawNode("Pipeline Output", allowDelete = false) { @@ -25,10 +30,14 @@ class OutputMatNode : DrawNode("Pipeline Output", allowDelete = false) { + input } + override var genSession: CodeGenSession? = null + override fun genCode(codeGen: CodeGen) = codeGen { processFrame { returnMethod(input.value(codeGen).value) // start code gen! } + + CodeGenSession() } override fun getOutputValueOf(codeGen: CodeGen, index: Int) = GenValue.None From b9c98dfe4ea9fe5e37434cc618478ca339ae9c1a Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Sun, 26 Sep 2021 20:45:06 -0600 Subject: [PATCH 32/56] Basic code gen working (import hooks pending) --- .../github/deltacv/easyvision/EasyVision.kt | 9 +++- .../deltacv/easyvision/attribute/Attribute.kt | 10 ++--- .../attribute/math/BooleanAttribute.kt | 27 +++++++++++- .../attribute/math/IntegerAttribute.kt | 24 +++++++++++ .../attribute/misc/EnumAttribute.kt | 43 ++++++++++++++++--- .../attribute/misc/ListAttribute.kt | 6 +++ .../deltacv/easyvision/codegen/Scope.kt | 2 - .../easyvision/codegen/type/GenType.kt | 9 +++- .../easyvision/exception/GenException.kt | 2 +- .../io/github/deltacv/easyvision/node/Link.kt | 2 +- .../io/github/deltacv/easyvision/node/Node.kt | 3 +- .../easyvision/node/math/SumIntegerNode.kt | 18 +++++++- .../easyvision/node/vision/CvtColorNode.kt | 21 +++++++-- .../easyvision/node/vision/MatNodes.kt | 13 +++--- 14 files changed, 157 insertions(+), 32 deletions(-) diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt index 15ac7115..c1fa8ea3 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt @@ -6,6 +6,7 @@ import imgui.app.Application import imgui.app.Configuration import imgui.flag.ImGuiCond import imgui.flag.ImGuiKey +import imgui.flag.ImGuiMouseButton import imgui.flag.ImGuiWindowFlags import io.github.deltacv.easyvision.codegen.* import io.github.deltacv.easyvision.node.NodeEditor @@ -14,6 +15,7 @@ import io.github.deltacv.easyvision.node.vision.Colors import io.github.deltacv.easyvision.node.vision.CvtColorNode import io.github.deltacv.easyvision.node.vision.InputMatNode import io.github.deltacv.easyvision.node.vision.OutputMatNode +import io.github.deltacv.easyvision.util.ElapsedTime import org.lwjgl.BufferUtils import org.lwjgl.glfw.GLFW import org.lwjgl.glfw.GLFW.glfwGetWindowSize @@ -89,11 +91,14 @@ class EasyVision : Application() { isDeleteReleased = false - if(ImGui.isKeyReleased(ImGuiKey.Z)) { + if(ImGui.isMouseReleased(ImGuiMouseButton.Right)) { + val timer = ElapsedTime() + val codeGen = CodeGen("TestPipeline") outputNode.genCode(codeGen) - println(codeGen) + println(codeGen.gen()) + println("took ${timer.seconds}") } } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt index f1dfdcb3..4111d611 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt @@ -22,8 +22,8 @@ abstract class Attribute : DrawableIdElement { val links = mutableListOf() val hasLink get() = links.isNotEmpty() - val isInput = mode == AttributeMode.INPUT - val isOutput = !isInput + val isInput by lazy { mode == AttributeMode.INPUT } + val isOutput by lazy { !isInput } abstract fun drawAttribute() @@ -68,14 +68,14 @@ abstract class Attribute : DrawableIdElement { val link = links[0] return if(link.aAttrib == this) { - link.aAttrib - } else link.bAttrib + link.bAttrib + } else link.aAttrib } fun raise(message: String): Nothing = throw AttributeGenException(this, message) fun raiseAssert(condition: Boolean, message: String) { - if(condition) { + if(!condition) { raise(message) } } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/BooleanAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/BooleanAttribute.kt index fb6c2bb2..3c7323de 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/BooleanAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/BooleanAttribute.kt @@ -2,10 +2,11 @@ package io.github.deltacv.easyvision.attribute.math import imgui.ImGui import imgui.type.ImBoolean -import imgui.type.ImInt import io.github.deltacv.easyvision.attribute.AttributeMode import io.github.deltacv.easyvision.attribute.Type import io.github.deltacv.easyvision.attribute.TypedAttribute +import io.github.deltacv.easyvision.codegen.CodeGen +import io.github.deltacv.easyvision.codegen.type.GenValue class BooleanAttribute( override val mode: AttributeMode, @@ -28,4 +29,28 @@ class BooleanAttribute( } } + override fun value(codeGen: CodeGen): GenValue.Boolean { + if(isInput) { + if(hasLink) { + val linkedAttrib = linkedAttribute() + + raiseAssert( + linkedAttrib != null, + "Boolean attribute must have another attribute attached" + ) + + val value = linkedAttrib!!.value(codeGen) + raiseAssert(value is GenValue.Boolean, "Attribute attached is not a Boolean") + + return value as GenValue.Boolean + } else { + return if (value.get()) { + GenValue.Boolean.True + } else GenValue.Boolean.False + } + } + + raise("Unexpected point reached while processing boolean attribute") + } + } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt index c2db1d52..816e3b8f 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt @@ -5,6 +5,8 @@ import imgui.type.ImInt import io.github.deltacv.easyvision.attribute.AttributeMode import io.github.deltacv.easyvision.attribute.Type import io.github.deltacv.easyvision.attribute.TypedAttribute +import io.github.deltacv.easyvision.codegen.CodeGen +import io.github.deltacv.easyvision.codegen.type.GenValue class IntAttribute( override val mode: AttributeMode, @@ -31,4 +33,26 @@ class IntAttribute( } } + override fun value(codeGen: CodeGen): GenValue.Int { + if(isInput) { + return if(hasLink) { + val linkedAttrib = linkedAttribute() + + raiseAssert( + linkedAttrib != null, + "Int attribute must have another attribute attached" + ) + + val value = linkedAttrib!!.value(codeGen) + raiseAssert(value is GenValue.Int, "Attribute attached is not a Int") + + value as GenValue.Int + } else { + GenValue.Int(value.get()) + } + } + + raise("Unexpected point reached while processing int attribute") + } + } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/EnumAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/EnumAttribute.kt index ea571547..ba828a4b 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/EnumAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/EnumAttribute.kt @@ -9,7 +9,7 @@ import io.github.deltacv.easyvision.attribute.TypedAttribute import io.github.deltacv.easyvision.codegen.CodeGen import io.github.deltacv.easyvision.codegen.type.GenValue -class EnumAttribute>( +class EnumAttribute>( override val mode: AttributeMode, val values: Array, override var variableName: String? @@ -29,15 +29,46 @@ class EnumAttribute>( override fun drawAttribute() { super.drawAttribute() - ImGui.pushItemWidth(110.0f) - ImGui.combo("", currentItem, valuesStrings) - ImGui.popItemWidth() + if(!hasLink) { + ImGui.pushItemWidth(110.0f) + ImGui.combo("", currentItem, valuesStrings) + ImGui.popItemWidth() + } } override fun acceptLink(other: Attribute) = other is EnumAttribute<*> && values[0]::class == other.values[0]::class - override fun value(codeGen: CodeGen): GenValue { - TODO("Not yet implemented") + @Suppress("UNCHECKED_CAST") + override fun value(codeGen: CodeGen): GenValue.Enum { + if(isInput) { + if(hasLink) { + val linkedAttrib = linkedAttribute() + + raiseAssert( + linkedAttrib != null, + "Enum attribute must have another attribute attached" + ) + + val value = linkedAttrib!!.value(codeGen) + raiseAssert(value is GenValue.Enum<*>, "Attribute attached is not a valid Enum") + + val valueEnum = value as GenValue.Enum<*> + val expectedClass = values[0]::class + + raiseAssert( + value.clazz == expectedClass, + "Enum attribute attached (${value.clazz}) is not the expected type of enum ($expectedClass)" + ) + + return valueEnum as GenValue.Enum + } else { + val value = values[currentItem.get()] + + return GenValue.Enum(value, value::class.java) + } + } + + raise("Unexpected point reached while processing enum attribute") } } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt index 1a58ed0b..a4af7a28 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt @@ -5,6 +5,8 @@ import io.github.deltacv.easyvision.attribute.Attribute import io.github.deltacv.easyvision.attribute.AttributeMode import io.github.deltacv.easyvision.attribute.Type import io.github.deltacv.easyvision.attribute.TypedAttribute +import io.github.deltacv.easyvision.codegen.CodeGen +import io.github.deltacv.easyvision.codegen.type.GenValue class ListAttribute( override val mode: AttributeMode, @@ -45,6 +47,10 @@ class ListAttribute( beforeHasLink = hasLink } + override fun value(codeGen: CodeGen): GenValue { + TODO("Not yet implemented") + } + override fun drawAttribute() { ImGui.text("[${elementType.name}] $variableName") diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Scope.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Scope.kt index 9c95c5bd..b2e758e4 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Scope.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Scope.kt @@ -145,8 +145,6 @@ class Scope(private val tabsCount: Int = 1) { fun newLineIfNotBlank() { val str = get() - println(str) - if(!str.endsWith("\n\n") && str.endsWith("\n")) { builder.appendLine() } else if(!str.endsWith("\n\n")) { diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/type/GenType.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/type/GenType.kt index 25f9cc7a..891cd1ae 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/type/GenType.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/type/GenType.kt @@ -7,7 +7,14 @@ sealed class GenValue { data class Mat(val value: Value, val color: Colors) : GenValue() - data class Enum>(val value: E) : GenValue() + data class Enum>(val value: E, val clazz: Class<*>) : GenValue() + + data class Int(val value: kotlin.Int) : GenValue() + + sealed class Boolean : GenValue() { + object True : Boolean() + object False : Boolean() + } object None : GenValue() diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/exception/GenException.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/exception/GenException.kt index 302a032d..a6894984 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/exception/GenException.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/exception/GenException.kt @@ -3,6 +3,6 @@ package io.github.deltacv.easyvision.exception import io.github.deltacv.easyvision.attribute.Attribute import io.github.deltacv.easyvision.node.Node -class NodeGenException(val node: Node, override val message: String) : RuntimeException(message) +class NodeGenException(val node: Node<*>, override val message: String) : RuntimeException(message) class AttributeGenException(val attribute: Attribute, override val message: String) : RuntimeException(message) \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Link.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Link.kt index c29ced68..f52f66eb 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Link.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Link.kt @@ -39,7 +39,7 @@ class Link(val a: Int, val b: Int) : DrawableIdElement { companion object { val links = IdElementContainer() - fun getLinksBetween(a: Node, b: Node): List { + fun getLinksBetween(a: Node<*>, b: Node<*>): List { val l = mutableListOf() for(link in links) { diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt index ea0f46b2..868e9c9d 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt @@ -86,7 +86,8 @@ abstract class Node( val session = codeGen.sessions[this] if(session == null) { - codeGen.sessions[this] = genCode(codeGen) + genSession = genCode(codeGen) + codeGen.sessions[this] = genSession!! } else { genSession = session as S } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt index a0d21341..08d8c64f 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt @@ -1,10 +1,14 @@ package io.github.deltacv.easyvision.node.math +import io.github.deltacv.easyvision.attribute.Attribute import io.github.deltacv.easyvision.node.DrawNode import io.github.deltacv.easyvision.attribute.math.IntAttribute import io.github.deltacv.easyvision.attribute.misc.ListAttribute +import io.github.deltacv.easyvision.codegen.CodeGen +import io.github.deltacv.easyvision.codegen.CodeGenSession +import io.github.deltacv.easyvision.codegen.type.GenValue -class SumIntegerNode : DrawNode("Sum Integer") { +class SumIntegerNode : DrawNode("Sum Integer") { override fun onEnable() { + ListAttribute(INPUT, IntAttribute, "Numbers") @@ -12,4 +16,16 @@ class SumIntegerNode : DrawNode("Sum Integer") { + IntAttribute(OUTPUT,"Result") } + class Session : CodeGenSession() { + + } + + override fun genCode(codeGen: CodeGen): Session { + TODO("Not yet implemented") + } + + override fun getOutputValueOf(codeGen: CodeGen, attrib: Attribute): GenValue { + TODO("Not yet implemented") + } + } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt index bcb27c51..80881e23 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt @@ -3,8 +3,7 @@ package io.github.deltacv.easyvision.node.vision import io.github.deltacv.easyvision.attribute.Attribute import io.github.deltacv.easyvision.attribute.misc.EnumAttribute import io.github.deltacv.easyvision.attribute.vision.MatAttribute -import io.github.deltacv.easyvision.codegen.CodeGen -import io.github.deltacv.easyvision.codegen.CodeGenSession +import io.github.deltacv.easyvision.codegen.* import io.github.deltacv.easyvision.codegen.type.GenValue import io.github.deltacv.easyvision.node.DrawNode @@ -31,8 +30,24 @@ class CvtColorNode : DrawNode("Convert Color") { val inputMat = input.value(codeGen) - processFrame { + val matColor = inputMat.color + val targetColor = convertTo.value(codeGen).value + if(inputMat.color != targetColor) { + val matName = "${targetColor.name.lowercase()}Mat" + + // create mat instance variable + private(matName, new("Mat")) + + processFrame { // add a cvtColor step in processFrame + "Imgproc.cvtColor"(inputMat.value, matName.v, cvtColorValue(matColor, targetColor)) + } + + session.outputMatValue = GenValue.Mat(matName.v, targetColor) // store data in the current session + } else { + // we don't need to do any processing if the mat is + // already of the color the user specified to convert to + session.outputMatValue = inputMat } session diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt index 30a62d01..ceb564bd 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt @@ -1,5 +1,6 @@ package io.github.deltacv.easyvision.node.vision +import io.github.deltacv.easyvision.attribute.Attribute import io.github.deltacv.easyvision.node.DrawNode import io.github.deltacv.easyvision.attribute.vision.MatAttribute import io.github.deltacv.easyvision.codegen.CodeGen @@ -7,9 +8,7 @@ import io.github.deltacv.easyvision.codegen.CodeGenSession import io.github.deltacv.easyvision.codegen.type.GenValue import io.github.deltacv.easyvision.codegen.v -class InputMatNode : DrawNode("Pipeline Input", allowDelete = false) { - - override var genSession: CodeGenSession? = null +class InputMatNode : DrawNode("Pipeline Input", allowDelete = false) { override fun onEnable() { + MatAttribute(OUTPUT, "Input") @@ -19,10 +18,10 @@ class InputMatNode : DrawNode("Pipeline Input", allowDelete = false) { raise("Input Mat node cannot generate code") } - override fun getOutputValueOf(codeGen: CodeGen, index: Int) = GenValue.Mat("input".v, Colors.RGB) + override fun getOutputValueOf(codeGen: CodeGen, attrib: Attribute) = GenValue.Mat("input".v, Colors.RGB) } -class OutputMatNode : DrawNode("Pipeline Output", allowDelete = false) { +class OutputMatNode : DrawNode("Pipeline Output", allowDelete = false) { val input = MatAttribute(INPUT, "Output") @@ -30,8 +29,6 @@ class OutputMatNode : DrawNode("Pipeline Output", allowDelete = false) { + input } - override var genSession: CodeGenSession? = null - override fun genCode(codeGen: CodeGen) = codeGen { processFrame { returnMethod(input.value(codeGen).value) // start code gen! @@ -40,5 +37,5 @@ class OutputMatNode : DrawNode("Pipeline Output", allowDelete = false) { CodeGenSession() } - override fun getOutputValueOf(codeGen: CodeGen, index: Int) = GenValue.None + override fun getOutputValueOf(codeGen: CodeGen, attrib: Attribute) = GenValue.None } \ No newline at end of file From 12c6f52cd4b1089e1f689a5298c63951938826f2 Mon Sep 17 00:00:00 2001 From: Sebastian Erives Date: Mon, 27 Sep 2021 07:14:38 -0600 Subject: [PATCH 33/56] Add more info about docs --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 918340a7..03220394 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,10 @@ Since OpenCV in Java uses a native library, which is platform specific, the simu * MacOS x64 (tested) * Linux x64 (tested for Ubuntu 20.04)
+## Downloading and documentation + +Follow the steps in [this page](https://deltacv.gitbook.io/eocv-sim/basics/downloading-eocv-sim) to download the sim. The rest of the documentation can also be found [there](https://deltacv.gitbook.io/eocv-sim/). + ## Adding EOCV-Sim as a dependency ### Gradle From 6701fb2301b8c90e39263a672e3b723ea080eb94 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Mon, 27 Sep 2021 07:22:50 -0600 Subject: [PATCH 34/56] Fix opencv dependency --- EOCV-Sim/build.gradle | 2 +- TeamCode/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/EOCV-Sim/build.gradle b/EOCV-Sim/build.gradle index 8e84e6ad..2b434e32 100644 --- a/EOCV-Sim/build.gradle +++ b/EOCV-Sim/build.gradle @@ -36,7 +36,7 @@ apply from: '../test-logging.gradle' dependencies { implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' - implementation 'org.openpnp:opencv:4.5.3-0' + implementation 'org.openpnp:opencv:4.5.1-2' implementation 'com.github.sarxos:webcam-capture:0.3.12' implementation 'info.picocli:picocli:4.6.1' diff --git a/TeamCode/build.gradle b/TeamCode/build.gradle index 1444caba..b788f1bd 100644 --- a/TeamCode/build.gradle +++ b/TeamCode/build.gradle @@ -6,7 +6,7 @@ plugins { apply from: '../build.common.gradle' dependencies { - implementation 'org.openpnp:opencv:4.5.3-0' + implementation 'org.openpnp:opencv:4.5.1-2' implementation project(':EOCV-Sim') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" From 2053fe51f8e8d0e1fc8dc2b58726d6eca7978703 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Mon, 27 Sep 2021 10:58:44 -0600 Subject: [PATCH 35/56] Improved formatting & code gen context, imports --- .../github/deltacv/easyvision/EasyVision.kt | 3 +- .../deltacv/easyvision/attribute/Attribute.kt | 6 +- .../easyvision/attribute/TypedAttribute.kt | 2 +- .../attribute/math/BooleanAttribute.kt | 6 +- .../attribute/math/IntegerAttribute.kt | 8 +- .../attribute/misc/EnumAttribute.kt | 22 ++- .../attribute/misc/ListAttribute.kt | 4 +- .../attribute/vision/MatAttribute.kt | 8 +- .../deltacv/easyvision/codegen/CodeGen.kt | 26 ++- .../easyvision/codegen/CodeGenSession.kt | 3 - .../github/deltacv/easyvision/codegen/Csv.kt | 3 + .../deltacv/easyvision/codegen/GenValue.kt | 23 +++ .../deltacv/easyvision/codegen/Scope.kt | 169 ------------------ .../deltacv/easyvision/codegen/Value.kt | 20 --- .../easyvision/codegen/dsl/CodeGenContext.kt | 3 + .../easyvision/codegen/dsl/ScopeContext.kt | 8 +- .../easyvision/codegen/type/GenType.kt | 21 --- .../io/github/deltacv/easyvision/node/Node.kt | 17 +- .../easyvision/node/math/SumIntegerNode.kt | 14 +- .../easyvision/node/vision/CvtColorNode.kt | 21 ++- .../easyvision/node/vision/MatNodes.kt | 18 +- 21 files changed, 129 insertions(+), 276 deletions(-) delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGenSession.kt create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/GenValue.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Scope.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Value.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/type/GenType.kt diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt index c1fa8ea3..ae85f44d 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt @@ -56,6 +56,7 @@ class EasyVision : Application() { SumIntegerNode().enable() SumIntegerNode().enable() + CvtColorNode().enable() CvtColorNode().enable() launch(this) @@ -95,7 +96,7 @@ class EasyVision : Application() { val timer = ElapsedTime() val codeGen = CodeGen("TestPipeline") - outputNode.genCode(codeGen) + outputNode.genCode(codeGen.currScopeProcessFrame) println(codeGen.gen()) println("took ${timer.seconds}") diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt index 4111d611..21765554 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt @@ -2,7 +2,7 @@ package io.github.deltacv.easyvision.attribute import imgui.extension.imnodes.ImNodes import io.github.deltacv.easyvision.codegen.CodeGen -import io.github.deltacv.easyvision.codegen.type.GenValue +import io.github.deltacv.easyvision.codegen.GenValue import io.github.deltacv.easyvision.exception.AttributeGenException import io.github.deltacv.easyvision.id.DrawableIdElement import io.github.deltacv.easyvision.node.Link @@ -82,8 +82,8 @@ abstract class Attribute : DrawableIdElement { abstract fun acceptLink(other: Attribute): Boolean - abstract fun value(codeGen: CodeGen): GenValue + abstract fun value(current: CodeGen.Current): GenValue - protected fun getOutputValue(codeGen: CodeGen) = parentNode.getOutputValueOf(codeGen, this) + protected fun getOutputValue(current: CodeGen.Current) = parentNode.getOutputValueOf(current, this) } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt index 07f3406e..46278e9b 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt @@ -2,7 +2,7 @@ package io.github.deltacv.easyvision.attribute import imgui.ImGui import io.github.deltacv.easyvision.codegen.CodeGen -import io.github.deltacv.easyvision.codegen.type.GenValue +import io.github.deltacv.easyvision.codegen.GenValue import io.github.deltacv.easyvision.exception.AttributeGenException import io.github.deltacv.easyvision.node.Link diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/BooleanAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/BooleanAttribute.kt index 3c7323de..b862eb14 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/BooleanAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/BooleanAttribute.kt @@ -6,7 +6,7 @@ import io.github.deltacv.easyvision.attribute.AttributeMode import io.github.deltacv.easyvision.attribute.Type import io.github.deltacv.easyvision.attribute.TypedAttribute import io.github.deltacv.easyvision.codegen.CodeGen -import io.github.deltacv.easyvision.codegen.type.GenValue +import io.github.deltacv.easyvision.codegen.GenValue class BooleanAttribute( override val mode: AttributeMode, @@ -29,7 +29,7 @@ class BooleanAttribute( } } - override fun value(codeGen: CodeGen): GenValue.Boolean { + override fun value(current: CodeGen.Current): GenValue.Boolean { if(isInput) { if(hasLink) { val linkedAttrib = linkedAttribute() @@ -39,7 +39,7 @@ class BooleanAttribute( "Boolean attribute must have another attribute attached" ) - val value = linkedAttrib!!.value(codeGen) + val value = linkedAttrib!!.value(current) raiseAssert(value is GenValue.Boolean, "Attribute attached is not a Boolean") return value as GenValue.Boolean diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt index 816e3b8f..a3869a1f 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt @@ -6,7 +6,7 @@ import io.github.deltacv.easyvision.attribute.AttributeMode import io.github.deltacv.easyvision.attribute.Type import io.github.deltacv.easyvision.attribute.TypedAttribute import io.github.deltacv.easyvision.codegen.CodeGen -import io.github.deltacv.easyvision.codegen.type.GenValue +import io.github.deltacv.easyvision.codegen.GenValue class IntAttribute( override val mode: AttributeMode, @@ -33,7 +33,7 @@ class IntAttribute( } } - override fun value(codeGen: CodeGen): GenValue.Int { + override fun value(current: CodeGen.Current): GenValue.Int { if(isInput) { return if(hasLink) { val linkedAttrib = linkedAttribute() @@ -43,8 +43,8 @@ class IntAttribute( "Int attribute must have another attribute attached" ) - val value = linkedAttrib!!.value(codeGen) - raiseAssert(value is GenValue.Int, "Attribute attached is not a Int") + val value = linkedAttrib!!.value(current) + raiseAssert(value is GenValue.Int, "Attribute attached is not an Int") value as GenValue.Int } else { diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/EnumAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/EnumAttribute.kt index ba828a4b..394c3eed 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/EnumAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/EnumAttribute.kt @@ -7,7 +7,7 @@ import io.github.deltacv.easyvision.attribute.AttributeMode import io.github.deltacv.easyvision.attribute.Type import io.github.deltacv.easyvision.attribute.TypedAttribute import io.github.deltacv.easyvision.codegen.CodeGen -import io.github.deltacv.easyvision.codegen.type.GenValue +import io.github.deltacv.easyvision.codegen.GenValue class EnumAttribute>( override val mode: AttributeMode, @@ -39,7 +39,9 @@ class EnumAttribute>( override fun acceptLink(other: Attribute) = other is EnumAttribute<*> && values[0]::class == other.values[0]::class @Suppress("UNCHECKED_CAST") - override fun value(codeGen: CodeGen): GenValue.Enum { + override fun value(current: CodeGen.Current): GenValue.Enum { + val expectedClass = values[0]::class + if(isInput) { if(hasLink) { val linkedAttrib = linkedAttribute() @@ -49,11 +51,10 @@ class EnumAttribute>( "Enum attribute must have another attribute attached" ) - val value = linkedAttrib!!.value(codeGen) + val value = linkedAttrib!!.value(current) raiseAssert(value is GenValue.Enum<*>, "Attribute attached is not a valid Enum") val valueEnum = value as GenValue.Enum<*> - val expectedClass = values[0]::class raiseAssert( value.clazz == expectedClass, @@ -66,9 +67,18 @@ class EnumAttribute>( return GenValue.Enum(value, value::class.java) } - } + } else { + val value = getOutputValue(current) + raiseAssert(value is GenValue.Enum<*>, "Value returned from the node is not an enum") - raise("Unexpected point reached while processing enum attribute") + val valueEnum = value as GenValue.Enum + raiseAssert( + value.clazz == expectedClass, + "Enum attribute returned from the node (${value.clazz}) is not the expected type of enum ($expectedClass)" + ) + + return valueEnum + } } } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt index a4af7a28..d4f7530d 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt @@ -6,7 +6,7 @@ import io.github.deltacv.easyvision.attribute.AttributeMode import io.github.deltacv.easyvision.attribute.Type import io.github.deltacv.easyvision.attribute.TypedAttribute import io.github.deltacv.easyvision.codegen.CodeGen -import io.github.deltacv.easyvision.codegen.type.GenValue +import io.github.deltacv.easyvision.codegen.GenValue class ListAttribute( override val mode: AttributeMode, @@ -47,7 +47,7 @@ class ListAttribute( beforeHasLink = hasLink } - override fun value(codeGen: CodeGen): GenValue { + override fun value(current: CodeGen.Current): GenValue { TODO("Not yet implemented") } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/MatAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/MatAttribute.kt index 9e97d6bf..160adba6 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/MatAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/MatAttribute.kt @@ -4,7 +4,7 @@ import io.github.deltacv.easyvision.attribute.TypedAttribute import io.github.deltacv.easyvision.attribute.AttributeMode import io.github.deltacv.easyvision.attribute.Type import io.github.deltacv.easyvision.codegen.CodeGen -import io.github.deltacv.easyvision.codegen.type.GenValue +import io.github.deltacv.easyvision.codegen.GenValue class MatAttribute( override val mode: AttributeMode, @@ -17,7 +17,7 @@ class MatAttribute( override fun new(mode: AttributeMode, variableName: String) = MatAttribute(mode, variableName) } - override fun value(codeGen: CodeGen): GenValue.Mat { + override fun value(current: CodeGen.Current): GenValue.Mat { if(isInput) { val linkedAttrib = linkedAttribute() @@ -26,12 +26,12 @@ class MatAttribute( "Mat attribute must have another attribute attached" ) - val value = linkedAttrib!!.value(codeGen) + val value = linkedAttrib!!.value(current) raiseAssert(value is GenValue.Mat, "Attribute attached is not a Mat") return value as GenValue.Mat } else { - val value = getOutputValue(codeGen) + val value = getOutputValue(current) raiseAssert(value is GenValue.Mat, "Value returned from the node is not a Mat") return value as GenValue.Mat diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt index f05f78ac..12317d65 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt @@ -1,5 +1,7 @@ package io.github.deltacv.easyvision.codegen +import io.github.deltacv.easyvision.codegen.build.Parameter +import io.github.deltacv.easyvision.codegen.build.Scope import io.github.deltacv.easyvision.codegen.dsl.CodeGenContext import io.github.deltacv.easyvision.node.Node @@ -13,14 +15,22 @@ class CodeGen(var className: String) { val classStartScope = Scope(1) val classEndScope = Scope(1) - val initScope = Scope(2) - val processFrameScope = Scope(2) - val viewportTappedScope = Scope(2) + val initScope = Scope(2) + val currScopeInit = Current(this, initScope) + + val processFrameScope = Scope(2) + val currScopeProcessFrame = Current(this, processFrameScope) + + val viewportTappedScope = Scope(2) + val currScopeViewportTapped = Current(this, viewportTappedScope) val sessions = mutableMapOf, CodeGenSession>() init { - importScope.import("org.openftc.easyopencv.OpenCvPipeline") + importScope.run { + import("org.openftc.easyopencv.OpenCvPipeline") + import("org.opencv.core.Mat") + } } fun gen(): String { @@ -76,4 +86,10 @@ class CodeGen(var className: String) { operator fun invoke(block: CodeGenContext.() -> T) = block(context) -} \ No newline at end of file + data class Current(val codeGen: CodeGen, val scope: Scope) { + operator fun invoke(scopeBlock: CodeGenContext.() -> T) = codeGen.invoke(scopeBlock) + } + +} + +open class CodeGenSession \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGenSession.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGenSession.kt deleted file mode 100644 index be025300..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGenSession.kt +++ /dev/null @@ -1,3 +0,0 @@ -package io.github.deltacv.easyvision.codegen - -open class CodeGenSession diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Csv.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Csv.kt index 5b2fff25..455f16b9 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Csv.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Csv.kt @@ -1,5 +1,8 @@ package io.github.deltacv.easyvision.codegen +import io.github.deltacv.easyvision.codegen.build.Parameter +import io.github.deltacv.easyvision.codegen.build.Value + fun Array.csv(): String { val builder = StringBuilder() diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/GenValue.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/GenValue.kt new file mode 100644 index 00000000..3c029818 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/GenValue.kt @@ -0,0 +1,23 @@ +package io.github.deltacv.easyvision.codegen + +import io.github.deltacv.easyvision.codegen.build.Value +import io.github.deltacv.easyvision.node.vision.Colors + +sealed class GenValue { + + data class Mat(val value: Value, val color: Colors) : GenValue() + + data class Enum>(val value: E, val clazz: Class<*>) : GenValue() + + data class Int(val value: kotlin.Int) : GenValue() + data class Float(val value: kotlin.Float) : GenValue() + data class Double(val value: kotlin.Double) : GenValue() + + sealed class Boolean(val value: kotlin.Boolean) : GenValue() { + object True : Boolean(true) + object False : Boolean(false) + } + + object None : GenValue() + +} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Scope.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Scope.kt deleted file mode 100644 index b2e758e4..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Scope.kt +++ /dev/null @@ -1,169 +0,0 @@ -package io.github.deltacv.easyvision.codegen - -import io.github.deltacv.easyvision.codegen.dsl.ScopeContext - -class Scope(private val tabsCount: Int = 1) { - - private var builder = StringBuilder() - - private val tabs by lazy { - val builder = StringBuilder() - - repeat(tabsCount) { - builder.append("\t") - } - - builder.toString() - } - - private val imports = mutableListOf() - - fun import(pkg: String) { - if(!imports.contains(pkg)) { - newStatement() - - imports.add(pkg) - - builder.append("import $pkg;") - } - } - - fun instanceVariable(vis: Visibility, name: String, - variable: Value, - isStatic: Boolean = false, isFinal: Boolean = false) { - newStatement() - - val modifiers = if(isStatic) " static" else "" + - if(isFinal) " final" else "" - - val ending = if(variable.value != null) "= ${variable.value};" else ";" - - builder.append("$tabs${vis.name.lowercase()}$modifiers ${variable.type} $name $ending") - } - - fun localVariable(name: String, variable: Value) { - newStatement() - - val ending = if(variable.value != null) "= ${variable.value};" else ";" - - builder.append("$tabs${variable.type} $name $ending") - } - - fun variableSet(name: String, v: Value) { - newStatement() - - builder.append("$tabs$name = ${v.value!!};") - } - - fun instanceVariableSet(name: String, v: Value) { - newStatement() - - builder.append("${tabs}this.$name = ${v.value!!};") - } - - fun methodCall(className: String, methodName: String, vararg parameters: Value) { - newStatement() - - builder.append("$tabs$className.$methodName(${parameters.csv()});") - } - - fun methodCall(methodName: String, vararg parameters: Value) { - newStatement() - - builder.append("$tabs$methodName(${parameters.csv()});") - } - - fun method( - vis: Visibility, returnType: String, name: String, body: Scope, - vararg parameters: Parameter, - isStatic: Boolean = false, isFinal: Boolean = false, isOverride: Boolean = false - ) { - newLineIfNotBlank() - - val static = if(isStatic) "static " else "" - val final = if(isFinal) "final " else "" - - if(isOverride) { - builder.append("$tabs@Override").appendLine() - } - - builder.append(""" - |$tabs${vis.name.lowercase()} $static$final$returnType $name(${parameters.csv()}) { - |$body - |$tabs} - """.trimMargin()) - } - - fun returnMethod(value: Value? = null) { - newStatement() - - if(value != null) { - builder.append("${tabs}return ${value.value!!};") - } else { - builder.append("${tabs}return;") - } - } - - fun clazz(vis: Visibility, name: String, body: Scope, - extends: Array = arrayOf(), implements: Array = arrayOf(), - isStatic: Boolean = false, isFinal: Boolean = false) { - - newStatement() - - val static = if(isStatic) "static " else "" - val final = if(isFinal) "final " else "" - - val e = if(extends.isNotEmpty()) "extends ${extends.csv()} " else "" - val i = if(implements.isNotEmpty()) "implements ${implements.csv()} " else "" - - val endWhitespaceLine = if(!body.get().endsWith("\n")) "\n" else "" - - builder.append(""" - |$tabs${vis.name.lowercase()} $static$final$name $e$i{ - |$body$endWhitespaceLine - |$tabs} - """.trimMargin()) - } - - fun enumClass(name: String, vararg values: String) { - newStatement() - - builder.append("${tabs}enum $name { ${values.csv()} }") - } - - fun scope(scope: Scope) { - newLineIfNotBlank() - builder.append(scope) - } - - fun newStatement() { - if(builder.isNotEmpty()) { - builder.appendLine() - } - } - - fun newLineIfNotBlank() { - val str = get() - - if(!str.endsWith("\n\n") && str.endsWith("\n")) { - builder.appendLine() - } else if(!str.endsWith("\n\n")) { - builder.append("\n") - } - } - - fun clear() = builder.clear() - - fun get() = builder.toString() - - override fun toString() = get() - - internal val context = ScopeContext(this) - - operator fun invoke(block: ScopeContext.() -> Unit) { - block(context) - } - -} - -data class Parameter(val type: String, val name: String) \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Value.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Value.kt deleted file mode 100644 index ed42ef03..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Value.kt +++ /dev/null @@ -1,20 +0,0 @@ -package io.github.deltacv.easyvision.codegen - -import io.github.deltacv.easyvision.node.vision.Colors - -fun new(type: String, vararg parameters: String) = Value(type, "new $type(${parameters.csv()})") - -fun value(type: String, value: String) = Value(type, value) - -fun callValue(methodName: String, returnType: String, vararg parameters: Value) = - Value(returnType, "$methodName(${parameters.csv()})") - -fun enumValue(type: String, constantName: String) = Value(type, "$type.$constantName") - -fun cvtColorValue(a: Colors, b: Colors) = Value("int", "Imgproc.COLOR_${a.name}2${b.name}") - -fun variable(type: String) = Value(type, null) - -val String.v get() = Value("", this) - -data class Value(val type: String, val value: String?) \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/CodeGenContext.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/CodeGenContext.kt index c6b41bba..dae623a3 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/CodeGenContext.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/CodeGenContext.kt @@ -1,6 +1,7 @@ package io.github.deltacv.easyvision.codegen.dsl import io.github.deltacv.easyvision.codegen.* +import io.github.deltacv.easyvision.codegen.build.* class CodeGenContext(val codeGen: CodeGen) { @@ -33,6 +34,8 @@ class CodeGenContext(val codeGen: CodeGen) { fun protected(name: String, v: Value) = codeGen.classStartScope.instanceVariable(Visibility.PROTECTED, name, v) + fun tryName(name: String) = codeGen.classStartScope.tryName(name) + operator fun String.invoke( vis: Visibility, returnType: String, vararg parameters: Parameter, diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/ScopeContext.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/ScopeContext.kt index 29b53608..ef350c71 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/ScopeContext.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/ScopeContext.kt @@ -1,11 +1,15 @@ package io.github.deltacv.easyvision.codegen.dsl -import io.github.deltacv.easyvision.codegen.Scope -import io.github.deltacv.easyvision.codegen.Value +import io.github.deltacv.easyvision.codegen.build.Scope +import io.github.deltacv.easyvision.codegen.build.Value import io.github.deltacv.easyvision.codegen.Visibility class ScopeContext(val scope: Scope) { + var appendWhiteline: Boolean + get() = scope.appendWhiteline + set(value) { scope.appendWhiteline = value } + operator fun String.invoke(vararg parameters: Value) { scope.methodCall(this, *parameters) } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/type/GenType.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/type/GenType.kt deleted file mode 100644 index 891cd1ae..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/type/GenType.kt +++ /dev/null @@ -1,21 +0,0 @@ -package io.github.deltacv.easyvision.codegen.type - -import io.github.deltacv.easyvision.codegen.Value -import io.github.deltacv.easyvision.node.vision.Colors - -sealed class GenValue { - - data class Mat(val value: Value, val color: Colors) : GenValue() - - data class Enum>(val value: E, val clazz: Class<*>) : GenValue() - - data class Int(val value: kotlin.Int) : GenValue() - - sealed class Boolean : GenValue() { - object True : Boolean() - object False : Boolean() - } - - object None : GenValue() - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt index 868e9c9d..50f4119e 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt @@ -7,7 +7,7 @@ import io.github.deltacv.easyvision.attribute.Attribute import io.github.deltacv.easyvision.attribute.AttributeMode import io.github.deltacv.easyvision.codegen.CodeGen import io.github.deltacv.easyvision.codegen.CodeGenSession -import io.github.deltacv.easyvision.codegen.type.GenValue +import io.github.deltacv.easyvision.codegen.GenValue import io.github.deltacv.easyvision.exception.NodeGenException interface Type { @@ -73,20 +73,19 @@ abstract class Node( operator fun Attribute.unaryPlus() = addAttribute(this) - abstract fun genCode(codeGen: CodeGen): S + abstract fun genCode(current: CodeGen.Current): S - /** - * The index corresponds to the order the attributes were added - * starting from 0 of course - */ - abstract fun getOutputValueOf(codeGen: CodeGen, attrib: Attribute): GenValue + open fun getOutputValueOf(current: CodeGen.Current, attrib: Attribute): GenValue { + raise("Node doesn't have output attributes") + } @Suppress("UNCHECKED_CAST") - fun genCodeIfNecessary(codeGen: CodeGen) { + fun genCodeIfNecessary(current: CodeGen.Current) { + val codeGen = current.codeGen val session = codeGen.sessions[this] if(session == null) { - genSession = genCode(codeGen) + genSession = genCode(current) codeGen.sessions[this] = genSession!! } else { genSession = session as S diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt index 08d8c64f..f449a141 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt @@ -6,25 +6,27 @@ import io.github.deltacv.easyvision.attribute.math.IntAttribute import io.github.deltacv.easyvision.attribute.misc.ListAttribute import io.github.deltacv.easyvision.codegen.CodeGen import io.github.deltacv.easyvision.codegen.CodeGenSession -import io.github.deltacv.easyvision.codegen.type.GenValue +import io.github.deltacv.easyvision.codegen.GenValue class SumIntegerNode : DrawNode("Sum Integer") { - override fun onEnable() { - + ListAttribute(INPUT, IntAttribute, "Numbers") + val numbers = ListAttribute(INPUT, IntAttribute, "Numbers") + val result = IntAttribute(OUTPUT,"Result") - + IntAttribute(OUTPUT,"Result") + override fun onEnable() { + + numbers + + result } class Session : CodeGenSession() { } - override fun genCode(codeGen: CodeGen): Session { + override fun genCode(current: CodeGen.Current): Session { TODO("Not yet implemented") } - override fun getOutputValueOf(codeGen: CodeGen, attrib: Attribute): GenValue { + override fun getOutputValueOf(current: CodeGen.Current, attrib: Attribute): GenValue { TODO("Not yet implemented") } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt index 80881e23..796f6ad7 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt @@ -4,7 +4,8 @@ import io.github.deltacv.easyvision.attribute.Attribute import io.github.deltacv.easyvision.attribute.misc.EnumAttribute import io.github.deltacv.easyvision.attribute.vision.MatAttribute import io.github.deltacv.easyvision.codegen.* -import io.github.deltacv.easyvision.codegen.type.GenValue +import io.github.deltacv.easyvision.codegen.CodeGenSession +import io.github.deltacv.easyvision.codegen.build.* import io.github.deltacv.easyvision.node.DrawNode enum class Colors { @@ -25,21 +26,23 @@ class CvtColorNode : DrawNode("Convert Color") { + output } - override fun genCode(codeGen: CodeGen) = codeGen { + override fun genCode(current: CodeGen.Current) = current { val session = Session() - val inputMat = input.value(codeGen) + val inputMat = input.value(current) + val targetColor = convertTo.value(current).value val matColor = inputMat.color - val targetColor = convertTo.value(codeGen).value - if(inputMat.color != targetColor) { - val matName = "${targetColor.name.lowercase()}Mat" + import("org.opencv.imgproc.Imgproc") + + if(matColor != targetColor) { + val matName = tryName("${targetColor.name.lowercase()}Mat") // create mat instance variable private(matName, new("Mat")) - processFrame { // add a cvtColor step in processFrame + current.scope { // add a cvtColor step in processFrame "Imgproc.cvtColor"(inputMat.value, matName.v, cvtColorValue(matColor, targetColor)) } @@ -53,8 +56,8 @@ class CvtColorNode : DrawNode("Convert Color") { session } - override fun getOutputValueOf(codeGen: CodeGen, attrib: Attribute): GenValue { - genCodeIfNecessary(codeGen) + override fun getOutputValueOf(current: CodeGen.Current, attrib: Attribute): GenValue { + genCodeIfNecessary(current) if(attrib == output) { return genSession!!.outputMatValue diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt index ceb564bd..ec24f5ce 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt @@ -5,8 +5,8 @@ import io.github.deltacv.easyvision.node.DrawNode import io.github.deltacv.easyvision.attribute.vision.MatAttribute import io.github.deltacv.easyvision.codegen.CodeGen import io.github.deltacv.easyvision.codegen.CodeGenSession -import io.github.deltacv.easyvision.codegen.type.GenValue -import io.github.deltacv.easyvision.codegen.v +import io.github.deltacv.easyvision.codegen.GenValue +import io.github.deltacv.easyvision.codegen.build.v class InputMatNode : DrawNode("Pipeline Input", allowDelete = false) { @@ -14,11 +14,12 @@ class InputMatNode : DrawNode("Pipeline Input", allowDelete = fa + MatAttribute(OUTPUT, "Input") } - override fun genCode(codeGen: CodeGen): CodeGenSession { + override fun genCode(current: CodeGen.Current): CodeGenSession { raise("Input Mat node cannot generate code") } - override fun getOutputValueOf(codeGen: CodeGen, attrib: Attribute) = GenValue.Mat("input".v, Colors.RGB) + override fun getOutputValueOf(current: CodeGen.Current, attrib: Attribute) = + GenValue.Mat("input".v, Colors.RGB) } class OutputMatNode : DrawNode("Pipeline Output", allowDelete = false) { @@ -29,13 +30,14 @@ class OutputMatNode : DrawNode("Pipeline Output", allowDelete = + input } - override fun genCode(codeGen: CodeGen) = codeGen { - processFrame { - returnMethod(input.value(codeGen).value) // start code gen! + override fun genCode(current: CodeGen.Current) = current { + current.scope { + returnMethod(input.value(current).value) // start code gen! + appendWhiteline = false } CodeGenSession() } - override fun getOutputValueOf(codeGen: CodeGen, attrib: Attribute) = GenValue.None + override fun getOutputValueOf(current: CodeGen.Current, attrib: Attribute) = GenValue.None } \ No newline at end of file From eb7d4b7a300212b8c69305852505675c0d37e10f Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Tue, 28 Sep 2021 13:10:00 -0600 Subject: [PATCH 36/56] Work on threshold node and range sliders --- .../github/deltacv/easyvision/EasyVision.kt | 9 +- .../deltacv/easyvision/attribute/Attribute.kt | 7 ++ .../attribute/misc/ListAttribute.kt | 83 +++++++++++++++---- .../attribute/vision/RangeAttribute.kt | 58 +++++++++++++ .../attribute/vision/ScalarAttribute.kt | 11 +++ .../deltacv/easyvision/codegen/CodeGen.kt | 4 +- .../deltacv/easyvision/gui/ExtraWidgets.kt | 22 +++++ .../easyvision/node/math/SumIntegerNode.kt | 5 +- .../easyvision/node/vision/CvtColorNode.kt | 7 +- .../easyvision/node/vision/MatNodes.kt | 9 +- .../easyvision/node/vision/ThresholdNode.kt | 22 +++++ 11 files changed, 209 insertions(+), 28 deletions(-) create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/RangeAttribute.kt create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/ScalarAttribute.kt create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/gui/ExtraWidgets.kt create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt index ae85f44d..dff69f57 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt @@ -9,12 +9,11 @@ import imgui.flag.ImGuiKey import imgui.flag.ImGuiMouseButton import imgui.flag.ImGuiWindowFlags import io.github.deltacv.easyvision.codegen.* +import io.github.deltacv.easyvision.id.IdElementContainer import io.github.deltacv.easyvision.node.NodeEditor import io.github.deltacv.easyvision.node.math.SumIntegerNode import io.github.deltacv.easyvision.node.vision.Colors -import io.github.deltacv.easyvision.node.vision.CvtColorNode -import io.github.deltacv.easyvision.node.vision.InputMatNode -import io.github.deltacv.easyvision.node.vision.OutputMatNode +import io.github.deltacv.easyvision.node.vision.* import io.github.deltacv.easyvision.util.ElapsedTime import org.lwjgl.BufferUtils import org.lwjgl.glfw.GLFW @@ -38,6 +37,8 @@ class EasyVision : Application() { return ImVec2(w.get(0).toFloat(), h.get(0).toFloat()) } + + val miscIds = IdElementContainer() } private var prevKeyCallback: GLFWKeyCallback? = null @@ -59,6 +60,8 @@ class EasyVision : Application() { CvtColorNode().enable() CvtColorNode().enable() + ThresholdNode().enable() + launch(this) editor.destroy() diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt index 21765554..88f24afd 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt @@ -24,10 +24,17 @@ abstract class Attribute : DrawableIdElement { val isInput by lazy { mode == AttributeMode.INPUT } val isOutput by lazy { !isInput } + + private var isFirstDraw = true abstract fun drawAttribute() override fun draw() { + if(isFirstDraw) { + enable() + isFirstDraw = false + } + if(mode == AttributeMode.INPUT) { ImNodes.beginInputAttribute(id) } else { diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt index d4f7530d..fd7cf7c1 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt @@ -8,10 +8,12 @@ import io.github.deltacv.easyvision.attribute.TypedAttribute import io.github.deltacv.easyvision.codegen.CodeGen import io.github.deltacv.easyvision.codegen.GenValue -class ListAttribute( +open class ListAttribute( override val mode: AttributeMode, val elementType: Type, - override var variableName: String? = null + override var variableName: String? = null, + length: Int? = null, + private val allowAddOrDelete: Boolean = true ) : TypedAttribute(Companion) { companion object: Type { @@ -20,9 +22,59 @@ class ListAttribute( } val listAttributes = mutableListOf() + val deleteQueue = mutableListOf() private var beforeHasLink = false - private var firstDraw = false + + private var previousLength: Int? = 0 + var fixedLength = length + set(value) { + field = value + onEnable() + } + + private val allowAod get() = allowAddOrDelete && fixedLength == null + + override fun onEnable() { + // oh god... (it's been only 10 minutes and i have already forgotten how this works) + if(previousLength != fixedLength) { + if(fixedLength != null && (previousLength == null || previousLength == 0)) { + repeat(fixedLength!!) { + createElement() + } + } else if(previousLength != null || previousLength != 0) { + val delta = (fixedLength ?: 0) - (previousLength ?: 0) + + if(delta < 0) { + repeat(-delta) { + val last = listAttributes[listAttributes.size - 1] + last.delete() + + listAttributes.remove(last) + deleteQueue.add(last) + } + } else { + repeat(delta) { + if(deleteQueue.isNotEmpty()) { + val last = deleteQueue[deleteQueue.size - 1] + last.restore() + + listAttributes.add(last) + deleteQueue.remove(last) + } else { + createElement() + } + } + } + } else { + for(attribute in listAttributes.toTypedArray()) { + attribute.delete() + } + } + } + + previousLength = fixedLength + } override fun draw() { super.draw() @@ -54,7 +106,7 @@ class ListAttribute( override fun drawAttribute() { ImGui.text("[${elementType.name}] $variableName") - if(!hasLink && elementType.allowsNew && mode == AttributeMode.INPUT) { + if(!hasLink && elementType.allowsNew && allowAod && mode == AttributeMode.INPUT) { // idk wat the frame height is, i just stole it from // https://github.com/ocornut/imgui/blob/7b8bc864e9af6c6c9a22125d65595d526ba674c5/imgui_widgets.cpp#L3439 @@ -66,16 +118,7 @@ class ListAttribute( if(ImGui.button("+", buttonSize, buttonSize)) { // creates a new element with the + button // uses the "new" function from the attribute's companion Type - val count = listAttributes.size.toString() - val elementName = count + if(count.length == 1) " " else "" - - val element = elementType.new(AttributeMode.INPUT, elementName) - element.enable() //enables the new element - - element.parentNode = parentNode - element.drawType = false // hides the variable type - - listAttributes.add(element) + createElement() } // display the - button only if the attributes list is not empty @@ -93,4 +136,16 @@ class ListAttribute( override fun acceptLink(other: Attribute) = other is ListAttribute && other.elementType == elementType + private fun createElement() { + val count = listAttributes.size.toString() + val elementName = count + if(count.length == 1) " " else "" + + val element = elementType.new(AttributeMode.INPUT, elementName) + element.enable() //enables the new element + + element.parentNode = parentNode + element.drawType = false // hides the variable type + + listAttributes.add(element) + } } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/RangeAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/RangeAttribute.kt new file mode 100644 index 00000000..7e8d2f6e --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/RangeAttribute.kt @@ -0,0 +1,58 @@ +package io.github.deltacv.easyvision.attribute.vision + +import imgui.type.ImInt +import io.github.deltacv.easyvision.EasyVision +import io.github.deltacv.easyvision.attribute.AttributeMode +import io.github.deltacv.easyvision.attribute.Type +import io.github.deltacv.easyvision.attribute.TypedAttribute +import io.github.deltacv.easyvision.codegen.CodeGen +import io.github.deltacv.easyvision.codegen.GenValue +import io.github.deltacv.easyvision.gui.ExtraWidgets + +class RangeAttribute( + override val mode: AttributeMode, + override var variableName: String? = null +) : TypedAttribute(Companion) { + + companion object : Type { + override val name = "Range" + + override fun new(mode: AttributeMode, variableName: String) = RangeAttribute(mode, variableName) + } + + var min = 0 + var max = 255 + + val minValue = ImInt(min) + val maxValue = ImInt(max) + + private val minId by EasyVision.miscIds.nextId() + private val maxId by EasyVision.miscIds.nextId() + + override fun drawAttribute() { + if(!hasLink) { + ExtraWidgets.rangeSliders( + min, max, + minValue, maxValue, + minId, maxId, + width = 95f + ) + + val mn = minValue.get() + val mx = maxValue.get() + + if(mn > mx) { + minValue.set(mx) + } + if(mx < mn) { + maxValue.set(mn) + } + } + } + + override fun value(current: CodeGen.Current): GenValue { + TODO("Not yet implemented") + } + + +} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/ScalarAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/ScalarAttribute.kt new file mode 100644 index 00000000..99207cca --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/ScalarAttribute.kt @@ -0,0 +1,11 @@ +package io.github.deltacv.easyvision.attribute.vision + +import io.github.deltacv.easyvision.attribute.AttributeMode +import io.github.deltacv.easyvision.attribute.misc.ListAttribute +import io.github.deltacv.easyvision.node.vision.Colors + +class ScalarAttribute( + mode: AttributeMode, + color: Colors, + variableName: String? = null +) : ListAttribute(mode, RangeAttribute, variableName, color.channels) \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt index 12317d65..6e0b9c90 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt @@ -92,4 +92,6 @@ class CodeGen(var className: String) { } -open class CodeGenSession \ No newline at end of file +interface CodeGenSession + +object NoSession : CodeGenSession \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/gui/ExtraWidgets.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/gui/ExtraWidgets.kt new file mode 100644 index 00000000..dd5e1372 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/gui/ExtraWidgets.kt @@ -0,0 +1,22 @@ +package io.github.deltacv.easyvision.gui + +import imgui.ImGui +import imgui.type.ImInt +import java.lang.Math.random + +object ExtraWidgets { + + fun rangeSliders(min: Int, max: Int, + minValue: ImInt, maxValue: ImInt, + minId: Int, maxId: Int, + width: Float = 110f) { + ImGui.pushItemWidth(width) + ImGui.sliderInt("###$minId", minValue.data, min, max) + + ImGui.sameLine() + + ImGui.sliderInt("###$maxId", maxValue.data, min, max) + ImGui.popItemWidth() + } + +} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt index f449a141..a4d48020 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt @@ -11,15 +11,14 @@ import io.github.deltacv.easyvision.codegen.GenValue class SumIntegerNode : DrawNode("Sum Integer") { val numbers = ListAttribute(INPUT, IntAttribute, "Numbers") - val result = IntAttribute(OUTPUT,"Result") + val result = IntAttribute(OUTPUT, "Result") override fun onEnable() { + numbers + result } - class Session : CodeGenSession() { - + class Session : CodeGenSession { } override fun genCode(current: CodeGen.Current): Session { diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt index 796f6ad7..f07f47e9 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt @@ -8,8 +8,9 @@ import io.github.deltacv.easyvision.codegen.CodeGenSession import io.github.deltacv.easyvision.codegen.build.* import io.github.deltacv.easyvision.node.DrawNode -enum class Colors { - RGB, BGR, HSV, YCrCb, LAB, GRAY +enum class Colors(channels: Int) { + RGBA(4), RGB(3), BGR(3), HSV(3), + YCrCb(3), LAB(3), GRAY(1) } class CvtColorNode : DrawNode("Convert Color") { @@ -66,7 +67,7 @@ class CvtColorNode : DrawNode("Convert Color") { raise("Attribute $attrib is not an output of this node or not handled by this") } - class Session : CodeGenSession() { + class Session : CodeGenSession { lateinit var outputMatValue: GenValue.Mat } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt index ec24f5ce..e1b30998 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt @@ -6,15 +6,16 @@ import io.github.deltacv.easyvision.attribute.vision.MatAttribute import io.github.deltacv.easyvision.codegen.CodeGen import io.github.deltacv.easyvision.codegen.CodeGenSession import io.github.deltacv.easyvision.codegen.GenValue +import io.github.deltacv.easyvision.codegen.NoSession import io.github.deltacv.easyvision.codegen.build.v -class InputMatNode : DrawNode("Pipeline Input", allowDelete = false) { +class InputMatNode : DrawNode("Pipeline Input", allowDelete = false) { override fun onEnable() { + MatAttribute(OUTPUT, "Input") } - override fun genCode(current: CodeGen.Current): CodeGenSession { + override fun genCode(current: CodeGen.Current): NoSession { raise("Input Mat node cannot generate code") } @@ -22,7 +23,7 @@ class InputMatNode : DrawNode("Pipeline Input", allowDelete = fa GenValue.Mat("input".v, Colors.RGB) } -class OutputMatNode : DrawNode("Pipeline Output", allowDelete = false) { +class OutputMatNode : DrawNode("Pipeline Output", allowDelete = false) { val input = MatAttribute(INPUT, "Output") @@ -36,7 +37,7 @@ class OutputMatNode : DrawNode("Pipeline Output", allowDelete = appendWhiteline = false } - CodeGenSession() + NoSession } override fun getOutputValueOf(current: CodeGen.Current, attrib: Attribute) = GenValue.None diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt new file mode 100644 index 00000000..73c24688 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt @@ -0,0 +1,22 @@ +package io.github.deltacv.easyvision.node.vision + +import io.github.deltacv.easyvision.attribute.misc.EnumAttribute +import io.github.deltacv.easyvision.attribute.vision.RangeAttribute +import io.github.deltacv.easyvision.attribute.vision.ScalarAttribute +import io.github.deltacv.easyvision.codegen.CodeGen +import io.github.deltacv.easyvision.codegen.NoSession +import io.github.deltacv.easyvision.node.DrawNode + +class ThresholdNode : DrawNode("Color Threshold") { + + val threshColor = EnumAttribute(INPUT, Colors.values(), "Space") + + override fun onEnable() { + + ScalarAttribute(INPUT, Colors.values()[0], "Test") + } + + override fun genCode(current: CodeGen.Current): NoSession { + TODO("Not yet implemented") + } + +} \ No newline at end of file From c2c51f23490c15be555349736a7c585c55261658 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Tue, 28 Sep 2021 18:35:31 -0600 Subject: [PATCH 37/56] Threshold node and stuff --- .../deltacv/easyvision/attribute/Attribute.kt | 19 +++ .../attribute/math/BooleanAttribute.kt | 7 +- .../attribute/math/IntegerAttribute.kt | 7 +- .../attribute/misc/ListAttribute.kt | 12 +- .../attribute/vision/RangeAttribute.kt | 28 ++++- .../attribute/vision/ScalarAttribute.kt | 41 +++++- .../deltacv/easyvision/codegen/GenValue.kt | 14 +++ .../deltacv/easyvision/gui/ExtraWidgets.kt | 21 ++++ .../io/github/deltacv/easyvision/node/Node.kt | 2 +- .../easyvision/node/vision/CvtColorNode.kt | 11 +- .../easyvision/node/vision/ThresholdNode.kt | 119 +++++++++++++++++- 11 files changed, 261 insertions(+), 20 deletions(-) diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt index 88f24afd..dbb61ae3 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt @@ -27,9 +27,28 @@ abstract class Attribute : DrawableIdElement { private var isFirstDraw = true + private var cancelNextDraw = false + var wasLastDrawCancelled = false + private set + abstract fun drawAttribute() + fun drawHere() { + draw() + cancelNextDraw = true + } + override fun draw() { + if(cancelNextDraw) { + cancelNextDraw = false + wasLastDrawCancelled = true + return + } + + if(wasLastDrawCancelled) { + wasLastDrawCancelled = false + } + if(isFirstDraw) { enable() isFirstDraw = false diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/BooleanAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/BooleanAttribute.kt index b862eb14..9d93370c 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/BooleanAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/BooleanAttribute.kt @@ -48,9 +48,12 @@ class BooleanAttribute( GenValue.Boolean.True } else GenValue.Boolean.False } - } + } else { + val value = getOutputValue(current) + raiseAssert(value is GenValue.Boolean, "Value returned from the node is not a Boolean") - raise("Unexpected point reached while processing boolean attribute") + return value as GenValue.Boolean + } } } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt index a3869a1f..3f73796f 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt @@ -50,9 +50,12 @@ class IntAttribute( } else { GenValue.Int(value.get()) } - } + } else { + val value = getOutputValue(current) + raiseAssert(value is GenValue.Int, "Value returned from the node is not an Int") - raise("Unexpected point reached while processing int attribute") + return value as GenValue.Int + } } } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt index fd7cf7c1..cb456094 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt @@ -79,7 +79,7 @@ open class ListAttribute( override fun draw() { super.draw() - for(attrib in listAttributes) { + for((i, attrib) in listAttributes.withIndex()) { if(beforeHasLink != hasLink) { if(hasLink) { // delete attributes if a link has been created @@ -92,6 +92,7 @@ open class ListAttribute( } if(!hasLink) { // only draw attributes if there's not a link attached + drawAttributeText(i) attrib.draw() } } @@ -99,9 +100,12 @@ open class ListAttribute( beforeHasLink = hasLink } - override fun value(current: CodeGen.Current): GenValue { - TODO("Not yet implemented") - } + open fun drawAttributeText(index: Int) { } + + override fun value(current: CodeGen.Current): GenValue = + // get the values of all the attributes and return a + // GenValue.List with the attribute values in an array + GenValue.List(listAttributes.map { it.value(current) }.toTypedArray()) override fun drawAttribute() { ImGui.text("[${elementType.name}] $variableName") diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/RangeAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/RangeAttribute.kt index 7e8d2f6e..f8eb02ae 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/RangeAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/RangeAttribute.kt @@ -50,8 +50,32 @@ class RangeAttribute( } } - override fun value(current: CodeGen.Current): GenValue { - TODO("Not yet implemented") + override fun value(current: CodeGen.Current): GenValue.Range { + if(isInput) { + return if(hasLink) { + val linkedAttrib = linkedAttribute() + + raiseAssert( + linkedAttrib != null, + "Range attribute must have another attribute attached" + ) + + val value = linkedAttrib!!.value(current) + raiseAssert(value is GenValue.Range, "Attribute attached is not a Range") + + value as GenValue.Range + } else { + GenValue.Range( + minValue.get().toDouble(), + maxValue.get().toDouble() + ) + } + } else { + val value = getOutputValue(current) + raiseAssert(value is GenValue.Range, "Value returned from the node is not a Range") + + return value as GenValue.Range + } } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/ScalarAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/ScalarAttribute.kt index 99207cca..48c52e4c 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/ScalarAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/ScalarAttribute.kt @@ -1,11 +1,50 @@ package io.github.deltacv.easyvision.attribute.vision +import imgui.ImGui import io.github.deltacv.easyvision.attribute.AttributeMode import io.github.deltacv.easyvision.attribute.misc.ListAttribute +import io.github.deltacv.easyvision.codegen.CodeGen +import io.github.deltacv.easyvision.codegen.GenValue import io.github.deltacv.easyvision.node.vision.Colors class ScalarAttribute( mode: AttributeMode, color: Colors, variableName: String? = null -) : ListAttribute(mode, RangeAttribute, variableName, color.channels) \ No newline at end of file +) : ListAttribute(mode, RangeAttribute, variableName, color.channels) { + + var color = color + set(value) { + fixedLength = value.channels + field = value + } + + override fun drawAttributeText(index: Int) { + if(index < color.channelNames.size) { + val name = color.channelNames[index] + val elementName = name + if(name.length == 1) " " else "" + + ImGui.text(elementName) + ImGui.sameLine() + } + } + + override fun value(current: CodeGen.Current): GenValue.ScalarRange { + val values = (super.value(current) as GenValue.List).elements + val ZERO = GenValue.Range.ZERO + + return GenValue.ScalarRange( + values.getOr(0, ZERO) as GenValue.Range, + values.getOr(1, ZERO) as GenValue.Range, + values.getOr(2, ZERO) as GenValue.Range, + values.getOr(3, ZERO) as GenValue.Range + ) + } + +} + +fun Array.getOr(index: Int, or: T) = try { + this[index] +} catch(ignored: ArrayIndexOutOfBoundsException) { + or +} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/GenValue.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/GenValue.kt index 3c029818..4c640876 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/GenValue.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/GenValue.kt @@ -13,11 +13,25 @@ sealed class GenValue { data class Float(val value: kotlin.Float) : GenValue() data class Double(val value: kotlin.Double) : GenValue() + data class Range(val min: kotlin.Double, val max: kotlin.Double) : GenValue(){ + companion object { + val ZERO = Range(0.0, 0.0) + } + } + + data class ScalarRange(val a: Range, val b: Range, val c: Range, val d: Range) : GenValue() { + companion object { + val ZERO = ScalarRange(Range.ZERO, Range.ZERO, Range.ZERO, Range.ZERO) + } + } + sealed class Boolean(val value: kotlin.Boolean) : GenValue() { object True : Boolean(true) object False : Boolean(false) } + data class List(val elements: Array) : GenValue() + object None : GenValue() } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/gui/ExtraWidgets.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/gui/ExtraWidgets.kt index dd5e1372..7ea8a4d6 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/gui/ExtraWidgets.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/gui/ExtraWidgets.kt @@ -19,4 +19,25 @@ object ExtraWidgets { ImGui.popItemWidth() } + private val valuesStringCache = mutableMapOf, Array>() + + fun > enumCombo(values: Array, currentItem: ImInt): T { + val clazz = values[0]::class.java + + val valuesStrings = if (valuesStringCache.containsKey(clazz)) { + valuesStringCache[clazz]!! + } else { + val v = values.map { + it.name + }.toTypedArray() + valuesStringCache[clazz] = v + + v + } + + ImGui.combo("", currentItem, valuesStrings) + + return values[currentItem.get()] + } + } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt index 50f4119e..a2fb36eb 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt @@ -30,7 +30,7 @@ abstract class Node( for((i, attribute) in nodeAttributes.withIndex()) { attribute.draw() - if(i < nodeAttributes.size - 1) { + if(i < nodeAttributes.size - 1 && !attribute.wasLastDrawCancelled) { ImGui.newLine() // make a new blank line if this isn't the last attribute } } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt index f07f47e9..16f1a207 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt @@ -8,9 +8,14 @@ import io.github.deltacv.easyvision.codegen.CodeGenSession import io.github.deltacv.easyvision.codegen.build.* import io.github.deltacv.easyvision.node.DrawNode -enum class Colors(channels: Int) { - RGBA(4), RGB(3), BGR(3), HSV(3), - YCrCb(3), LAB(3), GRAY(1) +enum class Colors(val channels: Int, val channelNames: Array) { + RGBA(4, arrayOf("R", "G", "B", "A")), + RGB(3, arrayOf("R", "G", "B")), + BGR(3, arrayOf("B", "G", "R")), + HSV(3, arrayOf("H", "S", "V")), + YCrCb(3, arrayOf("Y", "Cr", "Cb")), + LAB(3, arrayOf("L", "A", "B")), + GRAY(1, arrayOf("GRAY")) } class CvtColorNode : DrawNode("Convert Color") { diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt index 73c24688..be063dea 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt @@ -1,22 +1,131 @@ package io.github.deltacv.easyvision.node.vision +import imgui.ImGui +import imgui.type.ImInt +import io.github.deltacv.easyvision.attribute.Attribute import io.github.deltacv.easyvision.attribute.misc.EnumAttribute +import io.github.deltacv.easyvision.attribute.vision.MatAttribute import io.github.deltacv.easyvision.attribute.vision.RangeAttribute import io.github.deltacv.easyvision.attribute.vision.ScalarAttribute import io.github.deltacv.easyvision.codegen.CodeGen +import io.github.deltacv.easyvision.codegen.CodeGenSession +import io.github.deltacv.easyvision.codegen.GenValue import io.github.deltacv.easyvision.codegen.NoSession +import io.github.deltacv.easyvision.codegen.build.cvtColorValue +import io.github.deltacv.easyvision.codegen.build.new +import io.github.deltacv.easyvision.codegen.build.v +import io.github.deltacv.easyvision.gui.ExtraWidgets import io.github.deltacv.easyvision.node.DrawNode -class ThresholdNode : DrawNode("Color Threshold") { +class ThresholdNode : DrawNode("Color Threshold") { - val threshColor = EnumAttribute(INPUT, Colors.values(), "Space") + val input = MatAttribute(INPUT, "Input") + val scalar = ScalarAttribute(INPUT, Colors.values()[0], "Test") + val output = MatAttribute(OUTPUT, "Output") override fun onEnable() { - + ScalarAttribute(INPUT, Colors.values()[0], "Test") + + input + + scalar + + output } - override fun genCode(current: CodeGen.Current): NoSession { - TODO("Not yet implemented") + val colorValue = ImInt() + + private var lastColor = Colors.values()[0] + + override fun drawNode() { + input.drawHere() + + ImGui.newLine() + ImGui.text("(Enum) Color Space") + + ImGui.pushItemWidth(110.0f) + val color = ExtraWidgets.enumCombo(Colors.values(), colorValue) + ImGui.popItemWidth() + + ImGui.newLine() + + if(color != lastColor) { + scalar.color = color + } + + lastColor = color + } + + override fun genCode(current: CodeGen.Current) = current { + val session = Session() + + val range = scalar.value(current) + + var inputMat = input.value(current) + val matColor = inputMat.color + + val needsCvt = matColor != lastColor + + val cvtMat = tryName("${lastColor.name.lowercase()}Mat") + val thresholdTargetMat = tryName("${lastColor.name.lowercase()}BinaryMat") + + val lowerScalar = tryName("lower${lastColor.name}") + val upperScalar = tryName("upper${lastColor.name}") + + // add necessary imports + import("org.opencv.imgproc.Imgproc") + import("org.opencv.core.Scalar") + import("org.opencv.core.Core") + + // output mat target + private(thresholdTargetMat, new("Mat")) + + // lower color scalar + public(lowerScalar, + new("Scalar", + range.a.min.toString(), + range.b.min.toString(), + range.c.min.toString(), + range.d.min.toString(), + ) + ) + + // upper color scalar + public(upperScalar, + new("Scalar", + range.a.max.toString(), + range.b.max.toString(), + range.c.max.toString(), + range.d.max.toString(), + ) + ) + + if(needsCvt) { + private(cvtMat, new("Mat")) + } + + current.scope { + if(needsCvt) { + "Imgproc.cvtColor"(inputMat.value, cvtMat.v, cvtColorValue(matColor, lastColor)) + inputMat = GenValue.Mat(cvtMat.v, lastColor) + } + + "Core.inRange"(inputMat.value, lowerScalar.v, upperScalar.v, thresholdTargetMat.v) + } + + session.outputMat = GenValue.Mat(thresholdTargetMat.v, lastColor) + + session + } + + override fun getOutputValueOf(current: CodeGen.Current, attrib: Attribute): GenValue { + genCodeIfNecessary(current) + + if(attrib == output) { + return genSession!!.outputMat + } + + raise("Attribute $attrib is not an output of this node or not handled by this") + } + + class Session : CodeGenSession { + lateinit var outputMat: GenValue.Mat } } \ No newline at end of file From e0f6482e1f4c3750ae0cc3e708f83b5afb5018fa Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Tue, 28 Sep 2021 23:23:41 -0600 Subject: [PATCH 38/56] Threshold node fully working and fixed error with rgba cvt code --- EOCV-Sim/build.gradle | 2 +- .../resources/templates/gradle_workspace.zip | Bin 61310 -> 61335 bytes EasyVision/build.gradle | 2 + .../deltacv/easyvision/node/NodeEditor.kt | 2 +- .../easyvision/node/vision/CvtColorNode.kt | 12 +++++- .../easyvision/node/vision/MaskNode.kt | 4 ++ .../easyvision/node/vision/MatNodes.kt | 2 +- .../easyvision/node/vision/ThresholdNode.kt | 35 +++++++++++------- TeamCode/build.gradle | 2 +- build.gradle | 1 + 10 files changed, 43 insertions(+), 19 deletions(-) create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MaskNode.kt diff --git a/EOCV-Sim/build.gradle b/EOCV-Sim/build.gradle index 2b434e32..b5bd7370 100644 --- a/EOCV-Sim/build.gradle +++ b/EOCV-Sim/build.gradle @@ -36,7 +36,7 @@ apply from: '../test-logging.gradle' dependencies { implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' - implementation 'org.openpnp:opencv:4.5.1-2' + implementation "org.openpnp:opencv:$opencv_version" implementation 'com.github.sarxos:webcam-capture:0.3.12' implementation 'info.picocli:picocli:4.6.1' diff --git a/EOCV-Sim/src/main/resources/templates/gradle_workspace.zip b/EOCV-Sim/src/main/resources/templates/gradle_workspace.zip index 0050ab1ab8367b80bafe6f4843a9ba37c1d6a004..a0c577091640502f80a119913f7369a42ef62baa 100644 GIT binary patch delta 349 zcmex&k9qoi<_#h2yq;TZg1c2%W-Mo5V0b?{kzKexx8I)okb%gN?@s%i*%Ym2EP63% ztA#bkJHyH?>z~Pc&RY6qw{+l==KRWnJAeDQ(_URvwVK>^&Dr;+Mt91RNDt>(90%3U z*eU(72@@-j-66B|=DRSdxi3X{Q=QK#c5ah3WM4T?#&+>(@fC^M)boAgW

qW=hs`VGosRyLtg1HN>6TlmCm?i^2=8dj3*~M zy-r~I#xeOPhw|j(ul<y^td=ta?eX`rz1V*39%io4F k=1ul`r@{wKL! zyG@We{q4EwhuNjq+-#gRGg$Ihouc2ZJ=R@Z-{xOPI+S2Iqj^G2(Tmvu#zz@Wx!m_X z^7+>*9jj~0`1rT~{;|jI1po4`ohGlOe=u&A=3sh+=1i?ux?&8!RlNcduQQ_3lXG9` zFfvV^^(vjQdot7O2*wSQqhBX5-Q}2U@J3N+Ze`s zljGkeFd9!j@HUh&bh6mHD8`z}h411SS4_V6u9Ruji^=z2h)#}uZ^Cx_1u&2p00=FN AzW@LL diff --git a/EasyVision/build.gradle b/EasyVision/build.gradle index b1f3f2d5..9fdcd265 100644 --- a/EasyVision/build.gradle +++ b/EasyVision/build.gradle @@ -3,6 +3,8 @@ plugins { id 'org.jetbrains.kotlin.jvm' } +version = "1.0.0" + dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib" implementation "io.github.spair:imgui-java-app:1.84.1.0" diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt index a89095b9..8a643bce 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt @@ -61,7 +61,7 @@ class NodeEditor(val easyVision: EasyVision) { return // we can't link a node to itself! } - inputAttrib.links.forEach { + inputAttrib.links.toTypedArray().forEach { it.delete() // delete the existing link(s) of the input attribute if there's any } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt index 16f1a207..1b0a25f3 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt @@ -37,8 +37,16 @@ class CvtColorNode : DrawNode("Convert Color") { val inputMat = input.value(current) - val targetColor = convertTo.value(current).value - val matColor = inputMat.color + var targetColor = convertTo.value(current).value + var matColor = inputMat.color + + if(matColor != targetColor) { + if(matColor == Colors.RGBA && targetColor != Colors.RGB) { + matColor = Colors.RGB + } else if(matColor != Colors.RGB && targetColor == Colors.RGBA) { + targetColor = Colors.RGB + } + } import("org.opencv.imgproc.Imgproc") diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MaskNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MaskNode.kt new file mode 100644 index 00000000..92693228 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MaskNode.kt @@ -0,0 +1,4 @@ +package io.github.deltacv.easyvision.node.vision + +class MaskNode { +} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt index e1b30998..b9e6882d 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt @@ -20,7 +20,7 @@ class InputMatNode : DrawNode("Pipeline Input", allowDelete = false) } override fun getOutputValueOf(current: CodeGen.Current, attrib: Attribute) = - GenValue.Mat("input".v, Colors.RGB) + GenValue.Mat("input".v, Colors.RGBA) } class OutputMatNode : DrawNode("Pipeline Output", allowDelete = false) { diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt index be063dea..d03b276d 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt @@ -58,24 +58,31 @@ class ThresholdNode : DrawNode("Color Threshold") { val range = scalar.value(current) var inputMat = input.value(current) - val matColor = inputMat.color - - val needsCvt = matColor != lastColor + + var matColor = inputMat.color + var targetColor = lastColor + + if(matColor != targetColor) { + if(matColor == Colors.RGBA && targetColor != Colors.RGB) { + matColor = Colors.RGB + } else if(matColor != Colors.RGB && targetColor == Colors.RGBA) { + targetColor = Colors.RGB + } + } + + val needsCvt = matColor != targetColor - val cvtMat = tryName("${lastColor.name.lowercase()}Mat") - val thresholdTargetMat = tryName("${lastColor.name.lowercase()}BinaryMat") + val cvtMat = tryName("${targetColor.name.lowercase()}Mat") + val thresholdTargetMat = tryName("${targetColor.name.lowercase()}BinaryMat") - val lowerScalar = tryName("lower${lastColor.name}") - val upperScalar = tryName("upper${lastColor.name}") + val lowerScalar = tryName("lower${targetColor.name}") + val upperScalar = tryName("upper${targetColor.name}") // add necessary imports import("org.opencv.imgproc.Imgproc") import("org.opencv.core.Scalar") import("org.opencv.core.Core") - // output mat target - private(thresholdTargetMat, new("Mat")) - // lower color scalar public(lowerScalar, new("Scalar", @@ -99,17 +106,19 @@ class ThresholdNode : DrawNode("Color Threshold") { if(needsCvt) { private(cvtMat, new("Mat")) } + // output mat target + private(thresholdTargetMat, new("Mat")) current.scope { if(needsCvt) { - "Imgproc.cvtColor"(inputMat.value, cvtMat.v, cvtColorValue(matColor, lastColor)) - inputMat = GenValue.Mat(cvtMat.v, lastColor) + "Imgproc.cvtColor"(inputMat.value, cvtMat.v, cvtColorValue(matColor, targetColor)) + inputMat = GenValue.Mat(cvtMat.v, targetColor) } "Core.inRange"(inputMat.value, lowerScalar.v, upperScalar.v, thresholdTargetMat.v) } - session.outputMat = GenValue.Mat(thresholdTargetMat.v, lastColor) + session.outputMat = GenValue.Mat(thresholdTargetMat.v, targetColor) session } diff --git a/TeamCode/build.gradle b/TeamCode/build.gradle index b788f1bd..439195e2 100644 --- a/TeamCode/build.gradle +++ b/TeamCode/build.gradle @@ -6,7 +6,7 @@ plugins { apply from: '../build.common.gradle' dependencies { - implementation 'org.openpnp:opencv:4.5.1-2' + implementation "org.openpnp:opencv:$opencv_version" implementation project(':EOCV-Sim') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" diff --git a/build.gradle b/build.gradle index 1fc96b71..b873b758 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,7 @@ buildscript { ext { kotlin_version = "1.5.10" kotlinx_coroutines_version = "1.5.0-native-mt" + opencv_version = "4.5.1-2" env = findProperty('env') == 'release' ? 'release' : 'dev' From f9e688c3f1a7accd37325d98d3f3018b9128b04f Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Tue, 28 Sep 2021 23:38:24 -0600 Subject: [PATCH 39/56] Rename gitignored package --- .../deltacv/easyvision/codegen/CodeGen.kt | 4 +- .../github/deltacv/easyvision/codegen/Csv.kt | 4 +- .../deltacv/easyvision/codegen/GenValue.kt | 2 +- .../easyvision/codegen/dsl/CodeGenContext.kt | 2 +- .../easyvision/codegen/dsl/ScopeContext.kt | 4 +- .../deltacv/easyvision/codegen/parse/Scope.kt | 199 ++++++++++++++++++ .../deltacv/easyvision/codegen/parse/Value.kt | 23 ++ .../easyvision/node/vision/CvtColorNode.kt | 2 +- .../easyvision/node/vision/MatNodes.kt | 3 +- .../easyvision/node/vision/ThresholdNode.kt | 11 +- 10 files changed, 236 insertions(+), 18 deletions(-) create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/parse/Scope.kt create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/parse/Value.kt diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt index 6e0b9c90..56af4a87 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt @@ -1,7 +1,7 @@ package io.github.deltacv.easyvision.codegen -import io.github.deltacv.easyvision.codegen.build.Parameter -import io.github.deltacv.easyvision.codegen.build.Scope +import io.github.deltacv.easyvision.codegen.parse.Parameter +import io.github.deltacv.easyvision.codegen.parse.Scope import io.github.deltacv.easyvision.codegen.dsl.CodeGenContext import io.github.deltacv.easyvision.node.Node diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Csv.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Csv.kt index 455f16b9..7a8b5703 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Csv.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Csv.kt @@ -1,7 +1,7 @@ package io.github.deltacv.easyvision.codegen -import io.github.deltacv.easyvision.codegen.build.Parameter -import io.github.deltacv.easyvision.codegen.build.Value +import io.github.deltacv.easyvision.codegen.parse.Parameter +import io.github.deltacv.easyvision.codegen.parse.Value fun Array.csv(): String { val builder = StringBuilder() diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/GenValue.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/GenValue.kt index 4c640876..a0893a29 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/GenValue.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/GenValue.kt @@ -1,6 +1,6 @@ package io.github.deltacv.easyvision.codegen -import io.github.deltacv.easyvision.codegen.build.Value +import io.github.deltacv.easyvision.codegen.parse.Value import io.github.deltacv.easyvision.node.vision.Colors sealed class GenValue { diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/CodeGenContext.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/CodeGenContext.kt index dae623a3..e92e58ea 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/CodeGenContext.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/CodeGenContext.kt @@ -1,7 +1,7 @@ package io.github.deltacv.easyvision.codegen.dsl import io.github.deltacv.easyvision.codegen.* -import io.github.deltacv.easyvision.codegen.build.* +import io.github.deltacv.easyvision.codegen.parse.* class CodeGenContext(val codeGen: CodeGen) { diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/ScopeContext.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/ScopeContext.kt index ef350c71..b002efd5 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/ScopeContext.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/ScopeContext.kt @@ -1,7 +1,7 @@ package io.github.deltacv.easyvision.codegen.dsl -import io.github.deltacv.easyvision.codegen.build.Scope -import io.github.deltacv.easyvision.codegen.build.Value +import io.github.deltacv.easyvision.codegen.parse.Scope +import io.github.deltacv.easyvision.codegen.parse.Value import io.github.deltacv.easyvision.codegen.Visibility class ScopeContext(val scope: Scope) { diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/parse/Scope.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/parse/Scope.kt new file mode 100644 index 00000000..aa4f9606 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/parse/Scope.kt @@ -0,0 +1,199 @@ +package io.github.deltacv.easyvision.codegen.parse + +import io.github.deltacv.easyvision.codegen.dsl.ScopeContext +import io.github.deltacv.easyvision.codegen.* + +class Scope(private val tabsCount: Int = 1) { + + private var builder = StringBuilder() + + private val usedNames = mutableListOf() + + private val tabs by lazy { + val builder = StringBuilder() + + repeat(tabsCount) { + builder.append("\t") + } + + builder.toString() + } + + private val imports = mutableListOf() + + fun import(pkg: String) { + if(!imports.contains(pkg)) { + newStatement() + + imports.add(pkg) + + builder.append("import $pkg;") + } + } + + fun instanceVariable(vis: Visibility, name: String, + variable: Value, + isStatic: Boolean = false, isFinal: Boolean = false) { + newStatement() + usedNames.add(name) + + val modifiers = if(isStatic) " static" else "" + + if(isFinal) " final" else "" + + val ending = if(variable.value != null) "= ${variable.value};" else ";" + + builder.append("$tabs${vis.name.lowercase()}$modifiers ${variable.type} $name $ending") + } + + fun localVariable(name: String, variable: Value) { + newStatement() + usedNames.add(name) + + val ending = if(variable.value != null) "= ${variable.value};" else ";" + + builder.append("$tabs${variable.type} $name $ending") + } + + fun tryName(name: String): String { + if(!usedNames.contains(name)) { + return name + } else { + var count = 1 + + while(true) { + val newName = "$name$count" + + if(!usedNames.contains(newName)) { + return newName + } + + count++ + } + } + } + + fun variableSet(name: String, v: Value) { + newStatement() + + builder.append("$tabs$name = ${v.value!!};") + } + + fun instanceVariableSet(name: String, v: Value) { + newStatement() + + builder.append("${tabs}this.$name = ${v.value!!};") + } + + fun methodCall(className: String, methodName: String, vararg parameters: Value) { + newStatement() + + builder.append("$tabs$className.$methodName(${parameters.csv()});") + } + + fun methodCall(methodName: String, vararg parameters: Value) { + newStatement() + + builder.append("$tabs$methodName(${parameters.csv()});") + } + + fun method( + vis: Visibility, returnType: String, name: String, body: Scope, + vararg parameters: Parameter, + isStatic: Boolean = false, isFinal: Boolean = false, isOverride: Boolean = false + ) { + newLineIfNotBlank() + + val static = if(isStatic) "static " else "" + val final = if(isFinal) "final " else "" + + if(isOverride) { + builder.append("$tabs@Override").appendLine() + } + + builder.append(""" + |$tabs${vis.name.lowercase()} $static$final$returnType $name(${parameters.csv()}) { + |$body + |$tabs} + """.trimMargin()) + } + + fun returnMethod(value: Value? = null) { + newStatement() + + if(value != null) { + builder.append("${tabs}return ${value.value!!};") + } else { + builder.append("${tabs}return;") + } + } + + fun clazz(vis: Visibility, name: String, body: Scope, + extends: Array = arrayOf(), implements: Array = arrayOf(), + isStatic: Boolean = false, isFinal: Boolean = false) { + + newStatement() + + val static = if(isStatic) "static " else "" + val final = if(isFinal) "final " else "" + + val e = if(extends.isNotEmpty()) "extends ${extends.csv()} " else "" + val i = if(implements.isNotEmpty()) "implements ${implements.csv()} " else "" + + val endWhitespaceLine = if(!body.get().endsWith("\n")) "\n" else "" + + builder.append(""" + |$tabs${vis.name.lowercase()} $static${final}class $name $e$i{ + |$body$endWhitespaceLine + |$tabs} + """.trimMargin()) + } + + fun enumClass(name: String, vararg values: String) { + newStatement() + + builder.append("${tabs}enum $name { ${values.csv()} }") + } + + fun scope(scope: Scope) { + newLineIfNotBlank() + builder.append(scope) + } + + fun newStatement() { + if(builder.isNotEmpty()) { + builder.appendLine() + } + } + + fun newLineIfNotBlank() { + val str = get() + + if(!str.endsWith("\n\n") && str.endsWith("\n")) { + builder.appendLine() + } else if(!str.endsWith("\n\n")) { + builder.append("\n") + } + } + + fun clear() = builder.clear() + + fun get() = builder.toString() + + override fun toString() = get() + + internal val context = ScopeContext(this) + + var appendWhiteline = true + + operator fun invoke(block: ScopeContext.() -> Unit) { + block(context) + + if(appendWhiteline) { + newStatement() + } + appendWhiteline = true + } + +} + +data class Parameter(val type: String, val name: String) \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/parse/Value.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/parse/Value.kt new file mode 100644 index 00000000..3f5433d6 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/parse/Value.kt @@ -0,0 +1,23 @@ +package io.github.deltacv.easyvision.codegen.parse + +import io.github.deltacv.easyvision.codegen.csv +import io.github.deltacv.easyvision.node.vision.Colors + +fun new(type: String, vararg parameters: String) = Value(type, "new $type(${parameters.csv()})") + +fun value(type: String, value: String) = Value(type, value) + +fun callValue(methodName: String, returnType: String, vararg parameters: Value) = + Value(returnType, "$methodName(${parameters.csv()})") + +fun enumValue(type: String, constantName: String) = Value(type, "$type.$constantName") + +fun cvtColorValue(a: Colors, b: Colors) = Value("int", "Imgproc.COLOR_${a.name}2${b.name}") + +fun variable(type: String) = Value(type, null) + +val String.v get() = Value("", this) + +val Number.v get() = toString().v + +data class Value(val type: String, val value: String?) \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt index 1b0a25f3..d076a5d7 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt @@ -5,7 +5,7 @@ import io.github.deltacv.easyvision.attribute.misc.EnumAttribute import io.github.deltacv.easyvision.attribute.vision.MatAttribute import io.github.deltacv.easyvision.codegen.* import io.github.deltacv.easyvision.codegen.CodeGenSession -import io.github.deltacv.easyvision.codegen.build.* +import io.github.deltacv.easyvision.codegen.parse.* import io.github.deltacv.easyvision.node.DrawNode enum class Colors(val channels: Int, val channelNames: Array) { diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt index b9e6882d..ba465f6d 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt @@ -4,10 +4,9 @@ import io.github.deltacv.easyvision.attribute.Attribute import io.github.deltacv.easyvision.node.DrawNode import io.github.deltacv.easyvision.attribute.vision.MatAttribute import io.github.deltacv.easyvision.codegen.CodeGen -import io.github.deltacv.easyvision.codegen.CodeGenSession import io.github.deltacv.easyvision.codegen.GenValue import io.github.deltacv.easyvision.codegen.NoSession -import io.github.deltacv.easyvision.codegen.build.v +import io.github.deltacv.easyvision.codegen.parse.v class InputMatNode : DrawNode("Pipeline Input", allowDelete = false) { diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt index d03b276d..9844a249 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt @@ -3,24 +3,21 @@ package io.github.deltacv.easyvision.node.vision import imgui.ImGui import imgui.type.ImInt import io.github.deltacv.easyvision.attribute.Attribute -import io.github.deltacv.easyvision.attribute.misc.EnumAttribute import io.github.deltacv.easyvision.attribute.vision.MatAttribute -import io.github.deltacv.easyvision.attribute.vision.RangeAttribute import io.github.deltacv.easyvision.attribute.vision.ScalarAttribute import io.github.deltacv.easyvision.codegen.CodeGen import io.github.deltacv.easyvision.codegen.CodeGenSession import io.github.deltacv.easyvision.codegen.GenValue -import io.github.deltacv.easyvision.codegen.NoSession -import io.github.deltacv.easyvision.codegen.build.cvtColorValue -import io.github.deltacv.easyvision.codegen.build.new -import io.github.deltacv.easyvision.codegen.build.v +import io.github.deltacv.easyvision.codegen.parse.cvtColorValue +import io.github.deltacv.easyvision.codegen.parse.new +import io.github.deltacv.easyvision.codegen.parse.v import io.github.deltacv.easyvision.gui.ExtraWidgets import io.github.deltacv.easyvision.node.DrawNode class ThresholdNode : DrawNode("Color Threshold") { val input = MatAttribute(INPUT, "Input") - val scalar = ScalarAttribute(INPUT, Colors.values()[0], "Test") + val scalar = ScalarAttribute(INPUT, Colors.values()[0], "Threshold") val output = MatAttribute(OUTPUT, "Output") override fun onEnable() { From beebac9224c646642a449b3782804a2fcb0ca84f Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Wed, 29 Sep 2021 16:22:52 -0600 Subject: [PATCH 40/56] Changed traversing of node tree from left to right --- .../github/deltacv/easyvision/EasyVision.kt | 8 +-- .../deltacv/easyvision/attribute/Attribute.kt | 16 +++++ .../easyvision/attribute/TypedAttribute.kt | 1 + .../deltacv/easyvision/codegen/GenValue.kt | 17 ++++- .../io/github/deltacv/easyvision/node/Node.kt | 32 +++++++++- .../easyvision/node/vision/CvtColorNode.kt | 1 + .../easyvision/node/vision/MaskNode.kt | 63 ++++++++++++++++++- .../easyvision/node/vision/MatNodes.kt | 12 ++-- .../easyvision/node/vision/ThresholdNode.kt | 5 +- 9 files changed, 141 insertions(+), 14 deletions(-) diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt index dff69f57..184fa4ec 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt @@ -54,14 +54,12 @@ class EasyVision : Application() { inputNode.enable() outputNode.enable() - SumIntegerNode().enable() - SumIntegerNode().enable() - - CvtColorNode().enable() CvtColorNode().enable() ThresholdNode().enable() + MaskNode().enable() + launch(this) editor.destroy() @@ -99,7 +97,7 @@ class EasyVision : Application() { val timer = ElapsedTime() val codeGen = CodeGen("TestPipeline") - outputNode.genCode(codeGen.currScopeProcessFrame) + inputNode.startGen(codeGen.currScopeProcessFrame) println(codeGen.gen()) println("took ${timer.seconds}") diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt index dbb61ae3..e5f66d49 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt @@ -98,14 +98,30 @@ abstract class Attribute : DrawableIdElement { } else link.aAttrib } + fun linkedAttributes() = links.map { + if(it.aAttrib == this) { + it.bAttrib + } else it.aAttrib + } + fun raise(message: String): Nothing = throw AttributeGenException(this, message) + fun warn(message: String) { + println("WARN: $message") // TODO: Warnings system... + } + fun raiseAssert(condition: Boolean, message: String) { if(!condition) { raise(message) } } + fun warnAssert(condition: Boolean, message: String) { + if(!condition) { + warn(message) + } + } + abstract fun acceptLink(other: Attribute): Boolean abstract fun value(current: CodeGen.Current): GenValue diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt index 46278e9b..59a5502c 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt @@ -28,6 +28,7 @@ abstract class TypedAttribute(var type: Type) : Attribute() { override fun drawAttribute() { if(drawDescriptiveText) { + ImGui.alignTextToFramePadding() val t = if(drawType) { "(${type.name}) " } else "" diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/GenValue.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/GenValue.kt index a0893a29..6ca1f7bb 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/GenValue.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/GenValue.kt @@ -1,11 +1,26 @@ package io.github.deltacv.easyvision.codegen +import io.github.deltacv.easyvision.attribute.Attribute import io.github.deltacv.easyvision.codegen.parse.Value import io.github.deltacv.easyvision.node.vision.Colors sealed class GenValue { - data class Mat(val value: Value, val color: Colors) : GenValue() + data class Mat(val value: Value, val color: Colors, val isBinary: kotlin.Boolean = false) : GenValue() { + fun requireBinary(attribute: Attribute) { + attribute.warnAssert( + isBinary, + "Input Mat is not binary as required, this might cause runtime issues." + ) + } + + fun requireNonBinary(attribute: Attribute) { + attribute.warnAssert( + !isBinary, + "Input Mat is binary where it shouldn't be, this might cause runtime issues." + ) + } + } data class Enum>(val value: E, val clazz: Class<*>) : GenValue() diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt index a2fb36eb..71e9a0a3 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt @@ -80,6 +80,11 @@ abstract class Node( } @Suppress("UNCHECKED_CAST") + /** + * Generates code if there's not a session in the current CodeGen + * Automatically propagates to all the nodes attached to the output + * attributes after genCode finishes. Called by default on onPropagateReceive() + */ fun genCodeIfNecessary(current: CodeGen.Current) { val codeGen = current.codeGen val session = codeGen.sessions[this] @@ -87,19 +92,44 @@ abstract class Node( if(session == null) { genSession = genCode(current) codeGen.sessions[this] = genSession!! + propagate(current) } else { genSession = session as S } } + fun propagate(current: CodeGen.Current) { + for(attribute in attribs) { + if(attribute.mode == AttributeMode.OUTPUT) { + for(linkedAttribute in attribute.linkedAttributes()) { + linkedAttribute.parentNode.onPropagateReceive(current) + } + } + } + } + + open fun onPropagateReceive(current: CodeGen.Current) { + genCodeIfNecessary(current) + } + fun raise(message: String): Nothing = throw NodeGenException(this, message) + fun warn(message: String) { + println("WARN: $message") // TODO: Warnings system... + } + fun raiseAssert(condition: Boolean, message: String) { - if(condition) { + if(!condition) { raise(message) } } + fun warnAssert(condition: Boolean, message: String) { + if(!condition) { + warn(message) + } + } + companion object { val nodes = IdElementContainer>() val attributes = IdElementContainer() diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt index d076a5d7..de11be0e 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt @@ -36,6 +36,7 @@ class CvtColorNode : DrawNode("Convert Color") { val session = Session() val inputMat = input.value(current) + inputMat.requireNonBinary(input) var targetColor = convertTo.value(current).value var matColor = inputMat.color diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MaskNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MaskNode.kt index 92693228..28f985c9 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MaskNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MaskNode.kt @@ -1,4 +1,65 @@ package io.github.deltacv.easyvision.node.vision -class MaskNode { +import io.github.deltacv.easyvision.attribute.Attribute +import io.github.deltacv.easyvision.attribute.vision.MatAttribute +import io.github.deltacv.easyvision.codegen.CodeGen +import io.github.deltacv.easyvision.codegen.CodeGenSession +import io.github.deltacv.easyvision.codegen.GenValue +import io.github.deltacv.easyvision.codegen.parse.new +import io.github.deltacv.easyvision.codegen.parse.v +import io.github.deltacv.easyvision.node.DrawNode + +class MaskNode : DrawNode("Binary Mask"){ + + val inputMat = MatAttribute(INPUT, "Input") + val maskMat = MatAttribute(INPUT, "Binary Mask") + + val outputMat = MatAttribute(OUTPUT, "Output") + + override fun onEnable() { + + inputMat + + maskMat + + + outputMat + } + + override fun genCode(current: CodeGen.Current) = current { + val session = Session() + + val input = inputMat.value(current) + input.requireNonBinary(inputMat) + + val mask = maskMat.value(current) + mask.requireBinary(maskMat) + + val output = tryName("${input.value.value!!}Mask") + + import("org.opencv.core.Core") + + private(output, new("Mat")) + + current.scope { + "$output.release"() + "Core.bitwise_and"(input.value, input.value, output.v, mask.value) + } + + session.outputMat = GenValue.Mat(output.v, input.color) + + session + } + + override fun getOutputValueOf(current: CodeGen.Current, attrib: Attribute): GenValue { + genCodeIfNecessary(current) + + if(attrib == outputMat) { + return genSession!!.outputMat + } + + raise("Attribute $attrib is not an output of this node or not handled by this") + } + + class Session : CodeGenSession { + lateinit var outputMat: GenValue.Mat + } + } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt index ba465f6d..b366ec20 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt @@ -14,12 +14,16 @@ class InputMatNode : DrawNode("Pipeline Input", allowDelete = false) + MatAttribute(OUTPUT, "Input") } - override fun genCode(current: CodeGen.Current): NoSession { - raise("Input Mat node cannot generate code") + override fun genCode(current: CodeGen.Current) = NoSession + + fun startGen(current: CodeGen.Current) { + propagate(current) } - override fun getOutputValueOf(current: CodeGen.Current, attrib: Attribute) = - GenValue.Mat("input".v, Colors.RGBA) + val value = GenValue.Mat("input".v, Colors.RGBA) + + override fun getOutputValueOf(current: CodeGen.Current, + attrib: Attribute) = value } class OutputMatNode : DrawNode("Pipeline Output", allowDelete = false) { diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt index 9844a249..fc38b483 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt @@ -18,7 +18,7 @@ class ThresholdNode : DrawNode("Color Threshold") { val input = MatAttribute(INPUT, "Input") val scalar = ScalarAttribute(INPUT, Colors.values()[0], "Threshold") - val output = MatAttribute(OUTPUT, "Output") + val output = MatAttribute(OUTPUT, "Binary Output") override fun onEnable() { + input @@ -55,6 +55,7 @@ class ThresholdNode : DrawNode("Color Threshold") { val range = scalar.value(current) var inputMat = input.value(current) + inputMat.requireNonBinary(input) var matColor = inputMat.color var targetColor = lastColor @@ -115,7 +116,7 @@ class ThresholdNode : DrawNode("Color Threshold") { "Core.inRange"(inputMat.value, lowerScalar.v, upperScalar.v, thresholdTargetMat.v) } - session.outputMat = GenValue.Mat(thresholdTargetMat.v, targetColor) + session.outputMat = GenValue.Mat(thresholdTargetMat.v, targetColor, true) session } From 814b5310ec10a25ef7a5fe023b2d7a75430ae6e1 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Wed, 29 Sep 2021 23:22:55 -0600 Subject: [PATCH 41/56] Work on plus button for nodes list --- EasyVision/build.gradle | 2 + .../github/deltacv/easyvision/EasyVision.kt | 32 +++++++++++--- .../deltacv/easyvision/codegen/CodeGen.kt | 10 ++--- .../deltacv/easyvision/gui/ExtraWidgets.kt | 43 +++++++++++++++++++ .../deltacv/easyvision/node/NodeList.kt | 37 ++++++++++++++++ 5 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt diff --git a/EasyVision/build.gradle b/EasyVision/build.gradle index 9fdcd265..32bd5b38 100644 --- a/EasyVision/build.gradle +++ b/EasyVision/build.gradle @@ -3,6 +3,8 @@ plugins { id 'org.jetbrains.kotlin.jvm' } +apply from: '../build.common.gradle' + version = "1.0.0" dependencies { diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt index 184fa4ec..24ed2762 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt @@ -1,16 +1,18 @@ package io.github.deltacv.easyvision +import imgui.ImFont import imgui.ImGui import imgui.ImVec2 import imgui.app.Application import imgui.app.Configuration -import imgui.flag.ImGuiCond -import imgui.flag.ImGuiKey -import imgui.flag.ImGuiMouseButton -import imgui.flag.ImGuiWindowFlags +import imgui.flag.* +import imgui.type.ImFloat import io.github.deltacv.easyvision.codegen.* +import io.github.deltacv.easyvision.gui.ExtraWidgets +import io.github.deltacv.easyvision.gui.makeFont import io.github.deltacv.easyvision.id.IdElementContainer import io.github.deltacv.easyvision.node.NodeEditor +import io.github.deltacv.easyvision.node.NodeList import io.github.deltacv.easyvision.node.math.SumIntegerNode import io.github.deltacv.easyvision.node.vision.Colors import io.github.deltacv.easyvision.node.vision.* @@ -44,7 +46,10 @@ class EasyVision : Application() { private var prevKeyCallback: GLFWKeyCallback? = null val editor = NodeEditor(this) + val nodeList = NodeList() + lateinit var defaultFont: ImFont + val inputNode = InputMatNode() val outputNode = OutputMatNode() @@ -69,6 +74,16 @@ class EasyVision : Application() { config.title = "EasyVision" } + override fun initImGui(config: Configuration?) { + super.initImGui(config) + defaultFont = makeFont(13f) + nodeList.init() + } + + val splitterId by miscIds.nextId() + val splitterSize1 = ImFloat(300f) + val splitterSize2 = ImFloat(300f) + override fun process() { if(prevKeyCallback == null) { ptr = handle @@ -81,15 +96,22 @@ class EasyVision : Application() { val size = windowSize ImGui.setNextWindowSize(size.x, size.y, ImGuiCond.Always) + ImGui.pushFont(defaultFont) ImGui.begin("Editor", - ImGuiWindowFlags.NoResize or ImGuiWindowFlags.NoMove or ImGuiWindowFlags.NoCollapse + ImGuiWindowFlags.NoResize or ImGuiWindowFlags.NoMove + or ImGuiWindowFlags.NoCollapse or ImGuiWindowFlags.NoBringToFrontOnFocus ) editor.draw() ImGui.end() + ImGui.popFont() + + nodeList.draw() + ImGui.pushFont(defaultFont) PopupBuilder.draw() + ImGui.popFont() isDeleteReleased = false diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt index 56af4a87..df7b3631 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt @@ -53,12 +53,10 @@ class CodeGen(var className: String) { } val process = processFrameScope.get() - if(process.isNotBlank()) { - bodyScope.method( - Visibility.PUBLIC, "Mat", "processFrame", processFrameScope, - Parameter("Mat", "input"), isOverride = true - ) - } + bodyScope.method( + Visibility.PUBLIC, "Mat", "processFrame", processFrameScope, + Parameter("Mat", "input"), isOverride = true + ) val viewportTapped = viewportTappedScope.get() if(viewportTapped.isNotBlank()) { diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/gui/ExtraWidgets.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/gui/ExtraWidgets.kt index 7ea8a4d6..a490b4ee 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/gui/ExtraWidgets.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/gui/ExtraWidgets.kt @@ -1,6 +1,11 @@ package io.github.deltacv.easyvision.gui +import imgui.ImFont +import imgui.ImFontConfig import imgui.ImGui +import imgui.internal.ImRect +import imgui.internal.flag.ImGuiAxis +import imgui.type.ImFloat import imgui.type.ImInt import java.lang.Math.random @@ -40,4 +45,42 @@ object ExtraWidgets { return values[currentItem.get()] } + fun splitter(id: Int, splitVertically: Boolean, thickness: Float, + size1: ImFloat, size2: ImFloat, + minSize1: Float, minSize2: Float, + splitterLongAxisSize: Float = -1.0f): Boolean { + + val cursorPos = ImGui.getCursorPos() + + val minX = cursorPos.x + if(splitVertically) size1.get() else 0f + val minY = cursorPos.y + if(splitVertically) 0f else size1.get() + + val maxX = minX + imgui.internal.ImGui.calcItemSizeX( + if(splitVertically) thickness else splitterLongAxisSize, + if(splitVertically) splitterLongAxisSize else thickness, + 0f, 0f + ) + val maxY = minY + imgui.internal.ImGui.calcItemSizeY( + if(splitVertically) splitterLongAxisSize else thickness, + if(splitVertically) thickness else splitterLongAxisSize, + 0f, 0f + ) + + return imgui.internal.ImGui.splitterBehavior( + minX, minY, maxX, maxY, id, + if(splitVertically) ImGuiAxis.X else ImGuiAxis.Y, + size1, size2, minSize1, minSize2 + ) + } + +} + +fun makeFont(size: Float): ImFont { + val fontConfig = ImFontConfig() + fontConfig.sizePixels = size + fontConfig.oversampleH = 1 + fontConfig.oversampleV = 1 + fontConfig.pixelSnapH = false + + return ImGui.getIO().fonts.addFontDefault(fontConfig) } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt new file mode 100644 index 00000000..125cfe65 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt @@ -0,0 +1,37 @@ +package io.github.deltacv.easyvision.node + +import imgui.ImFont +import imgui.ImGui +import imgui.flag.ImGuiWindowFlags +import io.github.deltacv.easyvision.EasyVision +import io.github.deltacv.easyvision.gui.makeFont + +class NodeList { + + lateinit var buttonFont: ImFont + + val plusFontSize = 50f + + fun init() { + buttonFont = makeFont(plusFontSize) + } + + fun draw() { + val size = EasyVision.windowSize + ImGui.setNextWindowPos(size.x - plusFontSize * 2.2f, size.y - plusFontSize * 2.2f) + + ImGui.begin("floating", ImGuiWindowFlags.NoBackground + or ImGuiWindowFlags.NoTitleBar or ImGuiWindowFlags.NoDecoration or ImGuiWindowFlags.NoMove) + ImGui.pushFont(buttonFont) + + val buttonSize = ImGui.getFrameHeight() + + if(ImGui.button("+", buttonSize, buttonSize)) { + + } + + ImGui.popFont() + ImGui.end() + } + +} \ No newline at end of file From 14f45b71bb99b2ef92ebebac493bfdaf97b3b212 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Thu, 30 Sep 2021 11:05:45 -0600 Subject: [PATCH 42/56] Work on node list menu --- .../github/deltacv/easyvision/EasyVision.kt | 16 ++++- .../deltacv/easyvision/node/NodeList.kt | 62 ++++++++++++++++--- 2 files changed, 66 insertions(+), 12 deletions(-) diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt index 24ed2762..2c7190ef 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt @@ -46,7 +46,7 @@ class EasyVision : Application() { private var prevKeyCallback: GLFWKeyCallback? = null val editor = NodeEditor(this) - val nodeList = NodeList() + val nodeList = NodeList(this) lateinit var defaultFont: ImFont @@ -96,16 +96,17 @@ class EasyVision : Application() { val size = windowSize ImGui.setNextWindowSize(size.x, size.y, ImGuiCond.Always) - ImGui.pushFont(defaultFont) + //ImGui.pushFont(defaultFont) ImGui.begin("Editor", ImGuiWindowFlags.NoResize or ImGuiWindowFlags.NoMove or ImGuiWindowFlags.NoCollapse or ImGuiWindowFlags.NoBringToFrontOnFocus + or ImGuiWindowFlags.NoTitleBar or ImGuiWindowFlags.NoDecoration ) editor.draw() ImGui.end() - ImGui.popFont() + //ImGui.popFont() nodeList.draw() @@ -114,6 +115,8 @@ class EasyVision : Application() { ImGui.popFont() isDeleteReleased = false + isEscReleased = false + isSpaceReleased = false if(ImGui.isMouseReleased(ImGuiMouseButton.Right)) { val timer = ElapsedTime() @@ -127,6 +130,11 @@ class EasyVision : Application() { } var isDeleteReleased = false + private set + var isEscReleased = false + private set + var isSpaceReleased = false + private set private fun keyCallback(windowId: Long, key: Int, scancode: Int, action: Int, mods: Int) { if(prevKeyCallback != null) { @@ -134,6 +142,8 @@ class EasyVision : Application() { } isDeleteReleased = scancode == 119 && action == GLFW.GLFW_RELEASE + isEscReleased = scancode == 9 && action == GLFW.GLFW_RELEASE + isSpaceReleased = scancode == 65 && action == GLFW.GLFW_RELEASE } } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt index 125cfe65..82ae442a 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt @@ -2,36 +2,80 @@ package io.github.deltacv.easyvision.node import imgui.ImFont import imgui.ImGui +import imgui.extension.imnodes.ImNodes +import imgui.flag.ImGuiCol +import imgui.flag.ImGuiCond import imgui.flag.ImGuiWindowFlags import io.github.deltacv.easyvision.EasyVision import io.github.deltacv.easyvision.gui.makeFont -class NodeList { +class NodeList(val easyVision: EasyVision) { lateinit var buttonFont: ImFont val plusFontSize = 50f + private var isNodesListOpen = false + private var lastButton = false + fun init() { buttonFont = makeFont(plusFontSize) } fun draw() { val size = EasyVision.windowSize - ImGui.setNextWindowPos(size.x - plusFontSize * 2.2f, size.y - plusFontSize * 2.2f) - ImGui.begin("floating", ImGuiWindowFlags.NoBackground - or ImGuiWindowFlags.NoTitleBar or ImGuiWindowFlags.NoDecoration or ImGuiWindowFlags.NoMove) - ImGui.pushFont(buttonFont) + if(easyVision.isSpaceReleased) { + isNodesListOpen = true + } else if(easyVision.isEscReleased) { + isNodesListOpen = false + } + + if(isNodesListOpen) { + ImGui.setNextWindowPos(0f, 0f) + ImGui.setNextWindowSize(size.x, size.y, ImGuiCond.Always) + + ImGui.pushStyleColor(ImGuiCol.WindowBg, 0f, 0f, 0f, 0.5f) + + //ImGui.setNextWindowFocus() + ImGui.begin("nodes", + ImGuiWindowFlags.NoResize or ImGuiWindowFlags.NoMove + or ImGuiWindowFlags.NoCollapse or ImGuiWindowFlags.NoTitleBar + or ImGuiWindowFlags.NoDecoration //or ImGuiWindowFlags.NoBringToFrontOnFocus + ) + + drawNodesList() + + ImGui.end() + + ImGui.popStyleColor() + } - val buttonSize = ImGui.getFrameHeight() + ImGui.setNextWindowPos(size.x - plusFontSize * 2f, size.y - plusFontSize * 2f) + ImGui.setNextWindowFocus() - if(ImGui.button("+", buttonSize, buttonSize)) { + ImGui.begin( + "floating", ImGuiWindowFlags.NoBackground + or ImGuiWindowFlags.NoTitleBar or ImGuiWindowFlags.NoDecoration or ImGuiWindowFlags.NoMove + ) - } + ImGui.pushFont(buttonFont) - ImGui.popFont() + val buttonSize = ImGui.getFrameHeight() + + val button = ImGui.button(if(isNodesListOpen) "x" else "+", buttonSize, buttonSize) + + if (button != lastButton && button) { + isNodesListOpen = !isNodesListOpen + } + + lastButton = button + + ImGui.popFont() ImGui.end() } + private fun drawNodesList() { + } + } \ No newline at end of file From e40700e151fd6430c5491d3073856f3881b0f41d Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Thu, 30 Sep 2021 11:48:59 -0600 Subject: [PATCH 43/56] Fix focus issues and duplicatte code --- .../io/github/deltacv/easyvision/EasyVision.kt | 8 ++------ .../io/github/deltacv/easyvision/node/Node.kt | 17 ++++++++++++++--- .../github/deltacv/easyvision/node/NodeList.kt | 5 +++-- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt index 2c7190ef..46480048 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt @@ -80,10 +80,6 @@ class EasyVision : Application() { nodeList.init() } - val splitterId by miscIds.nextId() - val splitterSize1 = ImFloat(300f) - val splitterSize2 = ImFloat(300f) - override fun process() { if(prevKeyCallback == null) { ptr = handle @@ -96,7 +92,7 @@ class EasyVision : Application() { val size = windowSize ImGui.setNextWindowSize(size.x, size.y, ImGuiCond.Always) - //ImGui.pushFont(defaultFont) + ImGui.pushFont(defaultFont) ImGui.begin("Editor", ImGuiWindowFlags.NoResize or ImGuiWindowFlags.NoMove or ImGuiWindowFlags.NoCollapse or ImGuiWindowFlags.NoBringToFrontOnFocus @@ -106,7 +102,7 @@ class EasyVision : Application() { editor.draw() ImGui.end() - //ImGui.popFont() + ImGui.popFont() nodeList.draw() diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt index 71e9a0a3..6737dcdc 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt @@ -79,6 +79,8 @@ abstract class Node( raise("Node doesn't have output attributes") } + private var isCurrentlyGenCode = false + @Suppress("UNCHECKED_CAST") /** * Generates code if there's not a session in the current CodeGen @@ -90,9 +92,18 @@ abstract class Node( val session = codeGen.sessions[this] if(session == null) { - genSession = genCode(current) - codeGen.sessions[this] = genSession!! - propagate(current) + // prevents duplicate code in weird edge cases + // (it's so hard to consider and test every possibility with nodes...) + if(!isCurrentlyGenCode) { + isCurrentlyGenCode = true + + genSession = genCode(current) + codeGen.sessions[this] = genSession!! + + isCurrentlyGenCode = false + + propagate(current) + } } else { genSession = session as S } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt index 82ae442a..44b5382c 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt @@ -52,7 +52,9 @@ class NodeList(val easyVision: EasyVision) { } ImGui.setNextWindowPos(size.x - plusFontSize * 2f, size.y - plusFontSize * 2f) - ImGui.setNextWindowFocus() + if(isNodesListOpen) { + ImGui.setNextWindowFocus() + } ImGui.begin( "floating", ImGuiWindowFlags.NoBackground @@ -60,7 +62,6 @@ class NodeList(val easyVision: EasyVision) { ) ImGui.pushFont(buttonFont) - val buttonSize = ImGui.getFrameHeight() val button = ImGui.button(if(isNodesListOpen) "x" else "+", buttonSize, buttonSize) From 4b77ba97311a2371a0e5cdb51021944d1b05cb45 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Thu, 30 Sep 2021 17:52:31 -0600 Subject: [PATCH 44/56] add another node editor in noodes list --- .../deltacv/easyvision/node/NodeEditor.kt | 4 ++ .../deltacv/easyvision/node/NodeList.kt | 43 +++++++++++++++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt index 8a643bce..7ea842a8 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt @@ -2,6 +2,7 @@ package io.github.deltacv.easyvision.node import imgui.ImGui import imgui.extension.imnodes.ImNodes +import imgui.extension.imnodes.ImNodesContext import imgui.flag.ImGuiMouseButton import imgui.type.ImInt import io.github.deltacv.easyvision.EasyVision @@ -10,11 +11,14 @@ import io.github.deltacv.easyvision.attribute.AttributeMode class NodeEditor(val easyVision: EasyVision) { + val context = ImNodesContext() + fun init() { ImNodes.createContext() } fun draw() { + ImNodes.editorContextSet(context) ImNodes.beginNodeEditor() for(node in Node.nodes) { diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt index 44b5382c..26385d36 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt @@ -1,8 +1,13 @@ package io.github.deltacv.easyvision.node +import imgui.ImColor import imgui.ImFont import imgui.ImGui import imgui.extension.imnodes.ImNodes +import imgui.extension.imnodes.ImNodesContext +import imgui.extension.imnodes.flag.ImNodesAttributeFlags +import imgui.extension.imnodes.flag.ImNodesColorStyle +import imgui.extension.imnodes.flag.ImNodesStyleFlags import imgui.flag.ImGuiCol import imgui.flag.ImGuiCond import imgui.flag.ImGuiWindowFlags @@ -13,11 +18,14 @@ class NodeList(val easyVision: EasyVision) { lateinit var buttonFont: ImFont - val plusFontSize = 50f + val plusFontSize = 60f private var isNodesListOpen = false private var lastButton = false + + private lateinit var listContext: ImNodesContext + fun init() { buttonFont = makeFont(plusFontSize) } @@ -31,13 +39,14 @@ class NodeList(val easyVision: EasyVision) { isNodesListOpen = false } + // NODES LIST + if(isNodesListOpen) { ImGui.setNextWindowPos(0f, 0f) ImGui.setNextWindowSize(size.x, size.y, ImGuiCond.Always) - ImGui.pushStyleColor(ImGuiCol.WindowBg, 0f, 0f, 0f, 0.5f) + ImGui.pushStyleColor(ImGuiCol.WindowBg, 0f, 0f, 0f, 0.55f) // transparent dark nodes list window - //ImGui.setNextWindowFocus() ImGui.begin("nodes", ImGuiWindowFlags.NoResize or ImGuiWindowFlags.NoMove or ImGuiWindowFlags.NoCollapse or ImGuiWindowFlags.NoTitleBar @@ -51,7 +60,10 @@ class NodeList(val easyVision: EasyVision) { ImGui.popStyleColor() } - ImGui.setNextWindowPos(size.x - plusFontSize * 2f, size.y - plusFontSize * 2f) + // OPEN/CLOSE BUTTON + + ImGui.setNextWindowPos(size.x - plusFontSize * 1.8f, size.y - plusFontSize * 1.8f) + if(isNodesListOpen) { ImGui.setNextWindowFocus() } @@ -68,6 +80,9 @@ class NodeList(val easyVision: EasyVision) { if (button != lastButton && button) { isNodesListOpen = !isNodesListOpen + if(isNodesListOpen) { + listContext = ImNodesContext() + } } lastButton = button @@ -77,6 +92,26 @@ class NodeList(val easyVision: EasyVision) { } private fun drawNodesList() { + ImNodes.editorContextSet(listContext) + + ImNodes.getStyle().gridSpacing = 99999f // lol only way to make grid invisible + ImNodes.pushColorStyle(ImNodesColorStyle.GridBackground, ImColor.floatToColor(0f, 0f, 0f, 0f)) + + ImNodes.clearNodeSelection() + + ImNodes.beginNodeEditor() + ImNodes.beginNode(3213) + ImNodes.beginNodeTitleBar() + ImGui.text("yes") + ImNodes.endNodeTitleBar() + + ImGui.text("aaaaaaa") + ImNodes.endNode() + + ImNodes.endNodeEditor() + + ImNodes.getStyle().gridSpacing = 32f // back to normal + ImNodes.popColorStyle() } } \ No newline at end of file From 76a5d91e7baccf707e83f970192ce5f2a5e55756 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Thu, 30 Sep 2021 19:05:13 -0600 Subject: [PATCH 45/56] Node creation with reflection working --- .../deltacv/easyvision/attribute/Attribute.kt | 24 ++++--- .../attribute/misc/ListAttribute.kt | 2 +- .../deltacv/easyvision/node/DrawNode.kt | 10 +++ .../io/github/deltacv/easyvision/node/Node.kt | 8 ++- .../deltacv/easyvision/node/NodeList.kt | 67 ++++++++++++++++--- 5 files changed, 89 insertions(+), 22 deletions(-) diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt index e5f66d49..4e5b58c7 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt @@ -14,7 +14,7 @@ abstract class Attribute : DrawableIdElement { abstract val mode: AttributeMode - override val id by Node.attributes.nextId { this } + override val id by lazy { parentNode.attributesIdContainer.nextId(this).value } lateinit var parentNode: Node<*> internal set @@ -53,19 +53,23 @@ abstract class Attribute : DrawableIdElement { enable() isFirstDraw = false } - - if(mode == AttributeMode.INPUT) { - ImNodes.beginInputAttribute(id) - } else { - ImNodes.beginOutputAttribute(id) + + if(parentNode.drawAttributesCircles) { + if (mode == AttributeMode.INPUT) { + ImNodes.beginInputAttribute(id) + } else { + ImNodes.beginOutputAttribute(id) + } } drawAttribute() - if(mode == AttributeMode.INPUT) { - ImNodes.endInputAttribute() - } else { - ImNodes.endOutputAttribute() + if(parentNode.drawAttributesCircles) { + if (mode == AttributeMode.INPUT) { + ImNodes.endInputAttribute() + } else { + ImNodes.endOutputAttribute() + } } } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt index cb456094..7e40efb0 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt @@ -145,9 +145,9 @@ open class ListAttribute( val elementName = count + if(count.length == 1) " " else "" val element = elementType.new(AttributeMode.INPUT, elementName) + element.parentNode = parentNode element.enable() //enables the new element - element.parentNode = parentNode element.drawType = false // hides the variable type listAttributes.add(element) diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/DrawNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/DrawNode.kt index 9f2e12ba..5b3d9719 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/DrawNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/DrawNode.kt @@ -1,6 +1,7 @@ package io.github.deltacv.easyvision.node import imgui.ImGui +import imgui.ImVec2 import imgui.extension.imnodes.ImNodes import io.github.deltacv.easyvision.codegen.CodeGenSession @@ -9,7 +10,16 @@ abstract class DrawNode( allowDelete: Boolean = true ) : Node(allowDelete) { + var nextNodePosition: ImVec2? = null + override fun draw() { + nextNodePosition?.let { + ImNodes.setNodeScreenSpacePos(id, it.x, it.y) + nextNodePosition = null + } + + ImNodes.setNodeDraggable(id, true) + ImNodes.beginNode(id) if(title != null) { ImNodes.beginNodeTitleBar() diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt index 6737dcdc..fda179ea 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt @@ -1,6 +1,7 @@ package io.github.deltacv.easyvision.node import imgui.ImGui +import imgui.ImVec2 import io.github.deltacv.easyvision.id.DrawableIdElement import io.github.deltacv.easyvision.id.IdElementContainer import io.github.deltacv.easyvision.attribute.Attribute @@ -18,7 +19,12 @@ abstract class Node( private var allowDelete: Boolean = true ) : DrawableIdElement { - override val id by nodes.nextId { this } + var nodesIdContainer = nodes + var attributesIdContainer = attributes + + var drawAttributesCircles = true + + override val id by lazy { nodesIdContainer.nextId(this).value } private val attribs = mutableListOf() // internal mutable list val nodeAttributes = attribs as List // public read-only diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt index 26385d36..e57259db 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt @@ -3,6 +3,7 @@ package io.github.deltacv.easyvision.node import imgui.ImColor import imgui.ImFont import imgui.ImGui +import imgui.ImVec2 import imgui.extension.imnodes.ImNodes import imgui.extension.imnodes.ImNodesContext import imgui.extension.imnodes.flag.ImNodesAttributeFlags @@ -10,12 +11,22 @@ import imgui.extension.imnodes.flag.ImNodesColorStyle import imgui.extension.imnodes.flag.ImNodesStyleFlags import imgui.flag.ImGuiCol import imgui.flag.ImGuiCond +import imgui.flag.ImGuiMouseButton import imgui.flag.ImGuiWindowFlags import io.github.deltacv.easyvision.EasyVision +import io.github.deltacv.easyvision.attribute.Attribute import io.github.deltacv.easyvision.gui.makeFont +import io.github.deltacv.easyvision.id.IdElementContainer +import io.github.deltacv.easyvision.node.vision.CvtColorNode +import io.github.deltacv.easyvision.util.ElapsedTime class NodeList(val easyVision: EasyVision) { + companion object { + val listNodes = IdElementContainer>() + val listAttributes = IdElementContainer() + } + lateinit var buttonFont: ImFont val plusFontSize = 60f @@ -23,11 +34,19 @@ class NodeList(val easyVision: EasyVision) { private var isNodesListOpen = false private var lastButton = false + private val openButtonTimeout = ElapsedTime() private lateinit var listContext: ImNodesContext + val testNode = CvtColorNode() + fun init() { buttonFont = makeFont(plusFontSize) + + testNode.nodesIdContainer = listNodes + testNode.attributesIdContainer = listAttributes + testNode.drawAttributesCircles = false + testNode.enable() } fun draw() { @@ -78,8 +97,9 @@ class NodeList(val easyVision: EasyVision) { val button = ImGui.button(if(isNodesListOpen) "x" else "+", buttonSize, buttonSize) - if (button != lastButton && button) { - isNodesListOpen = !isNodesListOpen + if (button != lastButton && button && !isNodesListOpen && openButtonTimeout.millis > 200) { + isNodesListOpen = true + if(isNodesListOpen) { listContext = ImNodesContext() } @@ -98,20 +118,47 @@ class NodeList(val easyVision: EasyVision) { ImNodes.pushColorStyle(ImNodesColorStyle.GridBackground, ImColor.floatToColor(0f, 0f, 0f, 0f)) ImNodes.clearNodeSelection() + ImNodes.clearLinkSelection() ImNodes.beginNodeEditor() - ImNodes.beginNode(3213) - ImNodes.beginNodeTitleBar() - ImGui.text("yes") - ImNodes.endNodeTitleBar() - - ImGui.text("aaaaaaa") - ImNodes.endNode() - + for(node in listNodes) { + node.draw() + } ImNodes.endNodeEditor() ImNodes.getStyle().gridSpacing = 32f // back to normal ImNodes.popColorStyle() + + handleClick() + } + + fun handleClick() { + if(ImGui.isMouseClicked(ImGuiMouseButton.Left)) { + val hovered = ImNodes.getHoveredNode() + + if(hovered >= 0) { + val nodeClass = listNodes[hovered]!!::class.java + val instance = nodeClass.getConstructor().newInstance() + instance.enable() + + val nodePos = ImVec2() + val nodeDims = ImVec2() + ImNodes.getNodeScreenSpacePos(hovered, nodePos) + ImNodes.getNodeDimensions(hovered, nodeDims) + + val mousePos = ImGui.getMousePos() + + val newPosX = mousePos.x - nodePos.x + val newPosY = mousePos.y - nodePos.y + + if(instance is DrawNode<*>) { + instance.nextNodePosition = ImVec2(newPosX, newPosY) + } + } + + isNodesListOpen = false + openButtonTimeout.reset() + } } } \ No newline at end of file From 4ca214f384d142ffafce2c9fafab4b22ac29dcef Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Thu, 30 Sep 2021 21:17:47 -0600 Subject: [PATCH 46/56] Improve node position on creon creation --- .../io/github/deltacv/easyvision/EasyVision.kt | 6 +++--- .../io/github/deltacv/easyvision/node/NodeEditor.kt | 10 +++++++++- .../io/github/deltacv/easyvision/node/NodeList.kt | 13 ++++++++++++- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt index 46480048..03fd8298 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt @@ -19,9 +19,9 @@ import io.github.deltacv.easyvision.node.vision.* import io.github.deltacv.easyvision.util.ElapsedTime import org.lwjgl.BufferUtils import org.lwjgl.glfw.GLFW -import org.lwjgl.glfw.GLFW.glfwGetWindowSize -import org.lwjgl.glfw.GLFW.glfwSetKeyCallback +import org.lwjgl.glfw.GLFW.* import org.lwjgl.glfw.GLFWKeyCallback +import org.lwjgl.glfw.GLFWMouseButtonCallback class EasyVision : Application() { @@ -93,6 +93,7 @@ class EasyVision : Application() { ImGui.setNextWindowSize(size.x, size.y, ImGuiCond.Always) ImGui.pushFont(defaultFont) + ImGui.begin("Editor", ImGuiWindowFlags.NoResize or ImGuiWindowFlags.NoMove or ImGuiWindowFlags.NoCollapse or ImGuiWindowFlags.NoBringToFrontOnFocus @@ -141,7 +142,6 @@ class EasyVision : Application() { isEscReleased = scancode == 9 && action == GLFW.GLFW_RELEASE isSpaceReleased = scancode == 65 && action == GLFW.GLFW_RELEASE } - } fun main() { diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt index 7ea842a8..a3c0cb2d 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt @@ -19,6 +19,12 @@ class NodeEditor(val easyVision: EasyVision) { fun draw() { ImNodes.editorContextSet(context) + + if(easyVision.nodeList.isNodesListOpen) { + ImNodes.clearLinkSelection() + ImNodes.clearNodeSelection() + } + ImNodes.beginNodeEditor() for(node in Node.nodes) { @@ -95,7 +101,9 @@ class NodeEditor(val easyVision: EasyVision) { ImNodes.getSelectedNodes(selectedNodes) for(node in selectedNodes) { - Node.nodes[node]?.delete() + try { + Node.nodes[node]?.delete() + } catch(ignored: IndexOutOfBoundsException) {} } } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt index e57259db..0240caed 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt @@ -19,6 +19,9 @@ import io.github.deltacv.easyvision.gui.makeFont import io.github.deltacv.easyvision.id.IdElementContainer import io.github.deltacv.easyvision.node.vision.CvtColorNode import io.github.deltacv.easyvision.util.ElapsedTime +import java.awt.MouseInfo +import java.awt.Robot +import java.awt.event.InputEvent class NodeList(val easyVision: EasyVision) { @@ -31,7 +34,12 @@ class NodeList(val easyVision: EasyVision) { val plusFontSize = 60f - private var isNodesListOpen = false + var isNodesListOpen = false + private set + + private var wasNodesListOpen = false + val wasJustClosed get() = isNodesListOpen != wasNodesListOpen && !isNodesListOpen + private var lastButton = false private val openButtonTimeout = ElapsedTime() @@ -106,6 +114,7 @@ class NodeList(val easyVision: EasyVision) { } lastButton = button + wasNodesListOpen = isNodesListOpen ImGui.popFont() ImGui.end() @@ -132,6 +141,8 @@ class NodeList(val easyVision: EasyVision) { handleClick() } + private val robit = Robot() + fun handleClick() { if(ImGui.isMouseClicked(ImGuiMouseButton.Left)) { val hovered = ImNodes.getHoveredNode() From 0c7b3d0d736c7737ed91656ba1e01a771032b410 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Thu, 30 Sep 2021 22:57:56 -0600 Subject: [PATCH 47/56] Implemented proper dragging for new nodes --- .../deltacv/easyvision/node/DrawNode.kt | 41 ++++++++++++++++--- .../deltacv/easyvision/node/NodeList.kt | 17 ++++---- 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/DrawNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/DrawNode.kt index 5b3d9719..4d42e555 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/DrawNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/DrawNode.kt @@ -3,6 +3,7 @@ package io.github.deltacv.easyvision.node import imgui.ImGui import imgui.ImVec2 import imgui.extension.imnodes.ImNodes +import imgui.flag.ImGuiMouseButton import io.github.deltacv.easyvision.codegen.CodeGenSession abstract class DrawNode( @@ -12,14 +13,12 @@ abstract class DrawNode( var nextNodePosition: ImVec2? = null - override fun draw() { - nextNodePosition?.let { - ImNodes.setNodeScreenSpacePos(id, it.x, it.y) - nextNodePosition = null - } + var pinToMouse = false + private var lastPinToMouse = false - ImNodes.setNodeDraggable(id, true) + private var pinToMouseOffset = ImVec2() + override fun draw() { ImNodes.beginNode(id) if(title != null) { ImNodes.beginNodeTitleBar() @@ -30,6 +29,36 @@ abstract class DrawNode( drawNode() drawAttributes() ImNodes.endNode() + + nextNodePosition?.let { + ImNodes.setNodeScreenSpacePos(id, it.x, it.y) + nextNodePosition = null + } + + if(pinToMouse) { + val mousePos = ImGui.getMousePos() + + if(pinToMouse != lastPinToMouse) { + val nodeDims = ImVec2() + ImNodes.getNodeDimensions(id, nodeDims) + + pinToMouseOffset = ImVec2( + nodeDims.x / 2, + nodeDims.y / 2 + ) + } + + val newPosX = mousePos.x - pinToMouseOffset.x + val newPosY = mousePos.y - pinToMouseOffset.y + + ImNodes.setNodeEditorSpacePos(id, newPosX, newPosY) + + if(ImGui.isMouseReleased(ImGuiMouseButton.Left)) { + pinToMouse = false + } + } + + lastPinToMouse = pinToMouse } open fun drawNode() { } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt index 0240caed..fbc5f9d4 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt @@ -141,8 +141,6 @@ class NodeList(val easyVision: EasyVision) { handleClick() } - private val robit = Robot() - fun handleClick() { if(ImGui.isMouseClicked(ImGuiMouseButton.Left)) { val hovered = ImNodes.getHoveredNode() @@ -152,18 +150,17 @@ class NodeList(val easyVision: EasyVision) { val instance = nodeClass.getConstructor().newInstance() instance.enable() - val nodePos = ImVec2() - val nodeDims = ImVec2() - ImNodes.getNodeScreenSpacePos(hovered, nodePos) - ImNodes.getNodeDimensions(hovered, nodeDims) + if(instance is DrawNode<*>) { + val nodePos = ImVec2() + ImNodes.getNodeScreenSpacePos(hovered, nodePos) - val mousePos = ImGui.getMousePos() + val mousePos = ImGui.getMousePos() - val newPosX = mousePos.x - nodePos.x - val newPosY = mousePos.y - nodePos.y + val newPosX = mousePos.x - nodePos.x + val newPosY = mousePos.y - nodePos.y - if(instance is DrawNode<*>) { instance.nextNodePosition = ImVec2(newPosX, newPosY) + instance.pinToMouse = true } } From a445736428c96544045fc040d0ba906933243a8a Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Thu, 30 Sep 2021 23:24:06 -0600 Subject: [PATCH 48/56] Small details and button tooltip --- .../deltacv/easyvision/node/NodeEditor.kt | 5 ++ .../deltacv/easyvision/node/NodeList.kt | 53 ++++++++++++++----- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt index a3c0cb2d..2c7244c4 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt @@ -13,6 +13,9 @@ class NodeEditor(val easyVision: EasyVision) { val context = ImNodesContext() + var isNodeFocused = false + private set + fun init() { ImNodes.createContext() } @@ -36,6 +39,8 @@ class NodeEditor(val easyVision: EasyVision) { ImNodes.endNodeEditor() + isNodeFocused = ImNodes.numSelectedNodes() > 0 + handleDeleteLink() handleCreateLink() handleDeleteSelection() diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt index fbc5f9d4..626d6575 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt @@ -37,12 +37,10 @@ class NodeList(val easyVision: EasyVision) { var isNodesListOpen = false private set - private var wasNodesListOpen = false - val wasJustClosed get() = isNodesListOpen != wasNodesListOpen && !isNodesListOpen - private var lastButton = false private val openButtonTimeout = ElapsedTime() + private val hoveringPlusTime = ElapsedTime() private lateinit var listContext: ImNodesContext @@ -60,10 +58,12 @@ class NodeList(val easyVision: EasyVision) { fun draw() { val size = EasyVision.windowSize - if(easyVision.isSpaceReleased) { - isNodesListOpen = true - } else if(easyVision.isEscReleased) { - isNodesListOpen = false + if(!easyVision.editor.isNodeFocused && easyVision.isSpaceReleased) { + showList() + } + + if(easyVision.isEscReleased) { + closeList() } // NODES LIST @@ -105,18 +105,28 @@ class NodeList(val easyVision: EasyVision) { val button = ImGui.button(if(isNodesListOpen) "x" else "+", buttonSize, buttonSize) + ImGui.popFont() + if (button != lastButton && button && !isNodesListOpen && openButtonTimeout.millis > 200) { - isNodesListOpen = true + showList() + } - if(isNodesListOpen) { - listContext = ImNodesContext() + if(ImGui.isItemHovered()) { + if(hoveringPlusTime.millis > 500) { + ImGui.beginTooltip() + ImGui.text( + if(isNodesListOpen) { + "Press ESCAPE to close the nodes list" + } else "Press SPACE to open the nodes list" + ) + ImGui.endTooltip() } + } else { + hoveringPlusTime.reset() } lastButton = button - wasNodesListOpen = isNodesListOpen - ImGui.popFont() ImGui.end() } @@ -164,9 +174,26 @@ class NodeList(val easyVision: EasyVision) { } } - isNodesListOpen = false + closeList() openButtonTimeout.reset() } } + fun showList() { + if(!isNodesListOpen) { + if(::listContext.isInitialized) { + listContext.destroy() + } + listContext = ImNodesContext() + + isNodesListOpen = true + } + } + + fun closeList() { + if(isNodesListOpen) { + isNodesListOpen = false + } + } + } \ No newline at end of file From 4ec4284112ffe7786e4dd6d14f7775d81dcfd11e Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Fri, 1 Oct 2021 11:41:38 -0600 Subject: [PATCH 49/56] Add annotations for nodes --- .../github/deltacv/easyvision/EasyVision.kt | 8 +---- .../easyvision/{node => }/NodeEditor.kt | 7 +++-- .../deltacv/easyvision/{node => }/NodeList.kt | 10 ++----- .../github/deltacv/easyvision/NodeManager.kt | 7 +++++ .../easyvision/{ => gui}/PopupBuilder.kt | 3 +- .../github/deltacv/easyvision/node/AddNode.kt | 5 ++++ .../easyvision/node/math/SumIntegerNode.kt | 30 +++++++++++++++---- .../easyvision/node/vision/CvtColorNode.kt | 9 +++++- .../easyvision/node/vision/MaskNode.kt | 7 +++++ .../easyvision/node/vision/ThresholdNode.kt | 7 +++++ 10 files changed, 68 insertions(+), 25 deletions(-) rename EasyVision/src/main/kotlin/io/github/deltacv/easyvision/{node => }/NodeEditor.kt (92%) rename EasyVision/src/main/kotlin/io/github/deltacv/easyvision/{node => }/NodeList.kt (95%) create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/NodeManager.kt rename EasyVision/src/main/kotlin/io/github/deltacv/easyvision/{ => gui}/PopupBuilder.kt (96%) create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/AddNode.kt diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt index 03fd8298..dab1da58 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt @@ -6,22 +6,16 @@ import imgui.ImVec2 import imgui.app.Application import imgui.app.Configuration import imgui.flag.* -import imgui.type.ImFloat import io.github.deltacv.easyvision.codegen.* -import io.github.deltacv.easyvision.gui.ExtraWidgets +import io.github.deltacv.easyvision.gui.PopupBuilder import io.github.deltacv.easyvision.gui.makeFont import io.github.deltacv.easyvision.id.IdElementContainer -import io.github.deltacv.easyvision.node.NodeEditor -import io.github.deltacv.easyvision.node.NodeList -import io.github.deltacv.easyvision.node.math.SumIntegerNode -import io.github.deltacv.easyvision.node.vision.Colors import io.github.deltacv.easyvision.node.vision.* import io.github.deltacv.easyvision.util.ElapsedTime import org.lwjgl.BufferUtils import org.lwjgl.glfw.GLFW import org.lwjgl.glfw.GLFW.* import org.lwjgl.glfw.GLFWKeyCallback -import org.lwjgl.glfw.GLFWMouseButtonCallback class EasyVision : Application() { diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/NodeEditor.kt similarity index 92% rename from EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt rename to EasyVision/src/main/kotlin/io/github/deltacv/easyvision/NodeEditor.kt index 2c7244c4..018928d6 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/NodeEditor.kt @@ -1,13 +1,14 @@ -package io.github.deltacv.easyvision.node +package io.github.deltacv.easyvision import imgui.ImGui import imgui.extension.imnodes.ImNodes import imgui.extension.imnodes.ImNodesContext import imgui.flag.ImGuiMouseButton import imgui.type.ImInt -import io.github.deltacv.easyvision.EasyVision -import io.github.deltacv.easyvision.PopupBuilder +import io.github.deltacv.easyvision.gui.PopupBuilder import io.github.deltacv.easyvision.attribute.AttributeMode +import io.github.deltacv.easyvision.node.Link +import io.github.deltacv.easyvision.node.Node class NodeEditor(val easyVision: EasyVision) { diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/NodeList.kt similarity index 95% rename from EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt rename to EasyVision/src/main/kotlin/io/github/deltacv/easyvision/NodeList.kt index 626d6575..df519f39 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/NodeList.kt @@ -1,4 +1,4 @@ -package io.github.deltacv.easyvision.node +package io.github.deltacv.easyvision import imgui.ImColor import imgui.ImFont @@ -6,22 +6,18 @@ import imgui.ImGui import imgui.ImVec2 import imgui.extension.imnodes.ImNodes import imgui.extension.imnodes.ImNodesContext -import imgui.extension.imnodes.flag.ImNodesAttributeFlags import imgui.extension.imnodes.flag.ImNodesColorStyle -import imgui.extension.imnodes.flag.ImNodesStyleFlags import imgui.flag.ImGuiCol import imgui.flag.ImGuiCond import imgui.flag.ImGuiMouseButton import imgui.flag.ImGuiWindowFlags -import io.github.deltacv.easyvision.EasyVision import io.github.deltacv.easyvision.attribute.Attribute import io.github.deltacv.easyvision.gui.makeFont import io.github.deltacv.easyvision.id.IdElementContainer +import io.github.deltacv.easyvision.node.DrawNode +import io.github.deltacv.easyvision.node.Node import io.github.deltacv.easyvision.node.vision.CvtColorNode import io.github.deltacv.easyvision.util.ElapsedTime -import java.awt.MouseInfo -import java.awt.Robot -import java.awt.event.InputEvent class NodeList(val easyVision: EasyVision) { diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/NodeManager.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/NodeManager.kt new file mode 100644 index 00000000..1de5dfb8 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/NodeManager.kt @@ -0,0 +1,7 @@ +package io.github.deltacv.easyvision + +class NodeManager { + + + +} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/PopupBuilder.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/gui/PopupBuilder.kt similarity index 96% rename from EasyVision/src/main/kotlin/io/github/deltacv/easyvision/PopupBuilder.kt rename to EasyVision/src/main/kotlin/io/github/deltacv/easyvision/gui/PopupBuilder.kt index ee1cdc66..84f3ea9c 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/PopupBuilder.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/gui/PopupBuilder.kt @@ -1,6 +1,7 @@ -package io.github.deltacv.easyvision +package io.github.deltacv.easyvision.gui import imgui.ImGui +import io.github.deltacv.easyvision.EasyVision import io.github.deltacv.easyvision.util.ElapsedTime object PopupBuilder { diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/AddNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/AddNode.kt new file mode 100644 index 00000000..b4b94f50 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/AddNode.kt @@ -0,0 +1,5 @@ +package io.github.deltacv.easyvision.node + +enum class Category { CV_BASICS, MATH, MISC} + +annotation class AddNode(val name: String, val category: Category, val description: String = "") diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt index a4d48020..3314d039 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt @@ -7,8 +7,15 @@ import io.github.deltacv.easyvision.attribute.misc.ListAttribute import io.github.deltacv.easyvision.codegen.CodeGen import io.github.deltacv.easyvision.codegen.CodeGenSession import io.github.deltacv.easyvision.codegen.GenValue +import io.github.deltacv.easyvision.node.AddNode +import io.github.deltacv.easyvision.node.Category -class SumIntegerNode : DrawNode("Sum Integer") { +@AddNode( + name = "Sum Integers", + category = Category.MATH, + description = "Sums a list of integers and outputs the result" +) +class SumIntegerNode : DrawNode("Sum Integers") { val numbers = ListAttribute(INPUT, IntAttribute, "Numbers") val result = IntAttribute(OUTPUT, "Result") @@ -18,15 +25,26 @@ class SumIntegerNode : DrawNode("Sum Integer") { + result } - class Session : CodeGenSession { - } + override fun genCode(current: CodeGen.Current) = current { + val session = Session() + - override fun genCode(current: CodeGen.Current): Session { - TODO("Not yet implemented") + + session } override fun getOutputValueOf(current: CodeGen.Current, attrib: Attribute): GenValue { - TODO("Not yet implemented") + genCodeIfNecessary() + + if(attrib == result) { + return genSession!!.result + } + + raise("Attribute $attrib is not an output of this node or not handled by this") + } + + class Session : CodeGenSession { + lateinit var result: GenValue.Int } } \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt index de11be0e..d5bc4d62 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt @@ -6,6 +6,8 @@ import io.github.deltacv.easyvision.attribute.vision.MatAttribute import io.github.deltacv.easyvision.codegen.* import io.github.deltacv.easyvision.codegen.CodeGenSession import io.github.deltacv.easyvision.codegen.parse.* +import io.github.deltacv.easyvision.node.AddNode +import io.github.deltacv.easyvision.node.Category import io.github.deltacv.easyvision.node.DrawNode enum class Colors(val channels: Int, val channelNames: Array) { @@ -15,9 +17,14 @@ enum class Colors(val channels: Int, val channelNames: Array) { HSV(3, arrayOf("H", "S", "V")), YCrCb(3, arrayOf("Y", "Cr", "Cb")), LAB(3, arrayOf("L", "A", "B")), - GRAY(1, arrayOf("GRAY")) + GRAY(1, arrayOf("Gray")) } +@AddNode( + name = "Convert Color", + category = Category.CV_BASICS, + description = "Converts a Mat from its current color space to the specified color space. If the mat is already in the specified color space, no conversion is made." +) class CvtColorNode : DrawNode("Convert Color") { val input = MatAttribute(INPUT, "Input") diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MaskNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MaskNode.kt index 28f985c9..99588a36 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MaskNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MaskNode.kt @@ -7,8 +7,15 @@ import io.github.deltacv.easyvision.codegen.CodeGenSession import io.github.deltacv.easyvision.codegen.GenValue import io.github.deltacv.easyvision.codegen.parse.new import io.github.deltacv.easyvision.codegen.parse.v +import io.github.deltacv.easyvision.node.AddNode +import io.github.deltacv.easyvision.node.Category import io.github.deltacv.easyvision.node.DrawNode +@AddNode( + name = "Binary Mask", + category = Category.CV_BASICS, + description = "Takes a normal image and performs a mask based on a binary image, discards or includes areas from the normal image based on the binary image." +) class MaskNode : DrawNode("Binary Mask"){ val inputMat = MatAttribute(INPUT, "Input") diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt index fc38b483..b5306554 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt @@ -12,8 +12,15 @@ import io.github.deltacv.easyvision.codegen.parse.cvtColorValue import io.github.deltacv.easyvision.codegen.parse.new import io.github.deltacv.easyvision.codegen.parse.v import io.github.deltacv.easyvision.gui.ExtraWidgets +import io.github.deltacv.easyvision.node.AddNode +import io.github.deltacv.easyvision.node.Category import io.github.deltacv.easyvision.node.DrawNode +@AddNode( + name = "Color Threshold", + category = Category.CV_BASICS, + description = "Performs a threshold in the input image and returns a binary image, discarding the pixels that were outside the range in the color space specified." +) class ThresholdNode : DrawNode("Color Threshold") { val input = MatAttribute(INPUT, "Input") From b324cf3e4bdede69efe0d2a73f6247c4479555ee Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Fri, 1 Oct 2021 19:10:23 -0600 Subject: [PATCH 50/56] CApril tag plugin working on linux --- EOCV-Sim/build.gradle | 9 +- .../eocvsim/pipeline/PipelineScanner.kt | 3 +- .../EOCVSimUncaughtExceptionHandler.kt | 2 +- .../github/deltacv/easyvision/EasyVision.kt | 35 +- .../io/github/deltacv/easyvision/Project.kt | 4 + .../deltacv/easyvision/node/DrawNode.kt | 9 + .../easyvision/node/math/SumIntegerNode.kt | 2 +- .../easyvision/node/vision/MatNodes.kt | 19 ++ TeamCode/build.gradle | 3 + .../teamcode/AprilTagDetectionPipeline.java | 305 ++++++++++++++++++ build.gradle | 5 +- 11 files changed, 380 insertions(+), 16 deletions(-) create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/Project.kt create mode 100644 TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagDetectionPipeline.java diff --git a/EOCV-Sim/build.gradle b/EOCV-Sim/build.gradle index b5bd7370..7b215f57 100644 --- a/EOCV-Sim/build.gradle +++ b/EOCV-Sim/build.gradle @@ -38,6 +38,7 @@ dependencies { implementation "org.openpnp:opencv:$opencv_version" implementation 'com.github.sarxos:webcam-capture:0.3.12' + implementation "com.github.deltacv:AprilTagDesktop:$apriltag_plugin_version" implementation 'info.picocli:picocli:4.6.1' implementation 'com.google.code.gson:gson:2.8.7' @@ -69,8 +70,8 @@ task(writeBuildClassJava) { versionFile << "package com.github.serivesmejia.eocvsim;\n" + "\n" + "/*\n" + - " * Autogenerated file! Do not manually edit this file. This file\n" + - " * is regenerated any time the build task is run.\n" + + " * Autogenerated file! Do not manually edit this file, as\n" + + " * it is regenerated any time the build task is run.\n" + " *\n" + " * Based from PhotonVision PhotonVersion generator task\n"+ " */\n" + @@ -79,7 +80,9 @@ task(writeBuildClassJava) { " public static final String versionString = \"$version\";\n" + " public static final String standardVersionString = \"$standardVersion\";\n" + " public static final String buildDate = \"$date\";\n" + - " public static final boolean isDev = ${version.contains("dev")};\n" + + " public static final boolean isDev = ${version.contains("dev")};\n\n" + + " public static final String opencvVersion = \"$opencv_version\";\n" + + " public static final String apriltagPluginVersion = \"$apriltag_plugin_version\";\n" + "}" } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineScanner.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineScanner.kt index 8a6df940..8e3292eb 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineScanner.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineScanner.kt @@ -28,6 +28,7 @@ import com.github.serivesmejia.eocvsim.util.ReflectUtil import io.github.classgraph.ClassGraph import io.github.classgraph.ScanResult import org.openftc.easyopencv.OpenCvPipeline +import java.lang.reflect.Modifier @Suppress("UNCHECKED_CAST") class PipelineScanner(val scanInPackage: String = "org.firstinspires") { @@ -46,7 +47,7 @@ class PipelineScanner(val scanInPackage: String = "org.firstinspires") { continue //continue because we couldn't get the class... } - if(ReflectUtil.hasSuperclass(foundClass, OpenCvPipeline::class.java)) { + if(ReflectUtil.hasSuperclass(foundClass, OpenCvPipeline::class.java) && Modifier.isPublic(foundClass.modifiers)) { Log.info("PipelineScanner", "Found pipeline class ${foundClass.canonicalName}") callback(foundClass as Class); } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/EOCVSimUncaughtExceptionHandler.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/EOCVSimUncaughtExceptionHandler.kt index ae87f2ac..e82869d2 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/EOCVSimUncaughtExceptionHandler.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/exception/handling/EOCVSimUncaughtExceptionHandler.kt @@ -34,7 +34,7 @@ class EOCVSimUncaughtExceptionHandler private constructor() : Thread.UncaughtExc //Exit if uncaught exception happened in the main thread //since we would be basically in a deadlock state if that happened //or if we have a lotta uncaught exceptions. - if(t == currentMainThread || uncaughtExceptionsCount > MAX_UNCAUGHT_EXCEPTIONS_BEFORE_CRASH) { + if(t == currentMainThread || e !is Exception || uncaughtExceptionsCount > MAX_UNCAUGHT_EXCEPTIONS_BEFORE_CRASH) { CrashReport(e).saveCrashReport() Log.warn(TAG, "If this error persists, open an issue on EOCV-Sim's GitHub attaching the crash report file.") diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt index dab1da58..c3dd62d4 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt @@ -1,3 +1,26 @@ +/* + * Copyright (c) 2021 Sebastian Erives + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + package io.github.deltacv.easyvision import imgui.ImFont @@ -42,10 +65,10 @@ class EasyVision : Application() { val editor = NodeEditor(this) val nodeList = NodeList(this) - lateinit var defaultFont: ImFont + private lateinit var defaultFont: ImFont - val inputNode = InputMatNode() - val outputNode = OutputMatNode() + private val inputNode = InputMatNode() + private val outputNode = OutputMatNode() fun start() { editor.init() @@ -53,12 +76,6 @@ class EasyVision : Application() { inputNode.enable() outputNode.enable() - CvtColorNode().enable() - - ThresholdNode().enable() - - MaskNode().enable() - launch(this) editor.destroy() diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/Project.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/Project.kt new file mode 100644 index 00000000..d4265f22 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/Project.kt @@ -0,0 +1,4 @@ +package io.github.deltacv.easyvision + +class Project { +} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/DrawNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/DrawNode.kt index 4d42e555..b4090334 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/DrawNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/DrawNode.kt @@ -18,6 +18,10 @@ abstract class DrawNode( private var pinToMouseOffset = ImVec2() + var isFirstDraw = true + + open fun init() {} + override fun draw() { ImNodes.beginNode(id) if(title != null) { @@ -30,6 +34,11 @@ abstract class DrawNode( drawAttributes() ImNodes.endNode() + if(isFirstDraw) { + init() + isFirstDraw = false + } + nextNodePosition?.let { ImNodes.setNodeScreenSpacePos(id, it.x, it.y) nextNodePosition = null diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt index 3314d039..79d39679 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt @@ -34,7 +34,7 @@ class SumIntegerNode : DrawNode("Sum Integers") { } override fun getOutputValueOf(current: CodeGen.Current, attrib: Attribute): GenValue { - genCodeIfNecessary() + genCodeIfNecessary(current) if(attrib == result) { return genSession!!.result diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt index b366ec20..570486ce 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt @@ -1,5 +1,8 @@ package io.github.deltacv.easyvision.node.vision +import imgui.ImVec2 +import imgui.extension.imnodes.ImNodes +import io.github.deltacv.easyvision.EasyVision import io.github.deltacv.easyvision.attribute.Attribute import io.github.deltacv.easyvision.node.DrawNode import io.github.deltacv.easyvision.attribute.vision.MatAttribute @@ -10,6 +13,14 @@ import io.github.deltacv.easyvision.codegen.parse.v class InputMatNode : DrawNode("Pipeline Input", allowDelete = false) { + override fun init() { + val windowSize = EasyVision.windowSize + val nodeSize = ImVec2() + ImNodes.getNodeDimensions(id, nodeSize) + + ImNodes.setNodeScreenSpacePos(id, nodeSize.x * 0.5f, windowSize.y / 2f - nodeSize.y / 2) + } + override fun onEnable() { + MatAttribute(OUTPUT, "Input") } @@ -28,6 +39,14 @@ class InputMatNode : DrawNode("Pipeline Input", allowDelete = false) class OutputMatNode : DrawNode("Pipeline Output", allowDelete = false) { + override fun init() { + val windowSize = EasyVision.windowSize + val nodeSize = ImVec2() + ImNodes.getNodeDimensions(id, nodeSize) + + ImNodes.setNodeScreenSpacePos(id, windowSize.x - nodeSize.x * 1.5f , windowSize.y / 2f - nodeSize.y / 2) + } + val input = MatAttribute(INPUT, "Output") override fun onEnable() { diff --git a/TeamCode/build.gradle b/TeamCode/build.gradle index 439195e2..01d8c6ac 100644 --- a/TeamCode/build.gradle +++ b/TeamCode/build.gradle @@ -1,3 +1,5 @@ +import java.nio.file.Paths + plugins { id 'java' id 'org.jetbrains.kotlin.jvm' @@ -8,6 +10,7 @@ apply from: '../build.common.gradle' dependencies { implementation "org.openpnp:opencv:$opencv_version" implementation project(':EOCV-Sim') + implementation "com.github.deltacv:AprilTagDesktop:$apriltag_plugin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagDetectionPipeline.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagDetectionPipeline.java new file mode 100644 index 00000000..c03a56b8 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagDetectionPipeline.java @@ -0,0 +1,305 @@ +/* + * Copyright (c) 2021 OpenFTC Team + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.firstinspires.ftc.teamcode; + +import org.opencv.calib3d.Calib3d; +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.MatOfDouble; +import org.opencv.core.MatOfPoint2f; +import org.opencv.core.MatOfPoint3f; +import org.opencv.core.Point; +import org.opencv.core.Point3; +import org.opencv.core.Scalar; +import org.opencv.imgproc.Imgproc; +import org.openftc.apriltag.AprilTagDetection; +import org.openftc.apriltag.AprilTagDetectorJNI; +import org.openftc.easyopencv.OpenCvPipeline; + +import java.util.ArrayList; + +public class AprilTagDetectionPipeline extends OpenCvPipeline +{ + private long nativeApriltagPtr; + private Mat grey = new Mat(); + private ArrayList detections = new ArrayList<>(); + + private ArrayList detectionsUpdate = new ArrayList<>(); + private final Object detectionsUpdateSync = new Object(); + + Mat cameraMatrix; + + Scalar blue = new Scalar(7,197,235,255); + Scalar red = new Scalar(255,0,0,255); + Scalar green = new Scalar(0,255,0,255); + Scalar white = new Scalar(255,255,255,255); + + // Lens intrinsics + // UNITS ARE PIXELS + // NOTE: this calibration is for the C920 webcam at 800x448. + // You will need to do your own calibration for other configurations! + public static double fx = 578.272; + public static double fy = 578.272; + public static double cx = 402.145; + public static double cy = 221.506; + + // UNITS ARE METERS + public static double tagsize = 0.166; + + double tagsizeX; + double tagsizeY; + + private float decimation; + private boolean needToSetDecimation; + private final Object decimationSync = new Object(); + + public AprilTagDetectionPipeline() { + this.tagsizeX = tagsize; + this.tagsizeY = tagsize; + constructMatrix(); + } + + @Override + public void init(Mat frame) + { + // Allocate a native context object. See the corresponding deletion in the finalizer + nativeApriltagPtr = AprilTagDetectorJNI.createApriltagDetector(AprilTagDetectorJNI.TagFamily.TAG_36h11.string, 3, 3); + } + + @Override + public void finalize() + { + // Delete the native context we created in the init() function + AprilTagDetectorJNI.releaseApriltagDetector(nativeApriltagPtr); + } + + @Override + public Mat processFrame(Mat input) + { + // Convert to greyscale + Imgproc.cvtColor(input, grey, Imgproc.COLOR_RGBA2GRAY); + + synchronized (decimationSync) + { + if(needToSetDecimation) + { + AprilTagDetectorJNI.setApriltagDetectorDecimation(nativeApriltagPtr, decimation); + needToSetDecimation = false; + } + } + + // Run AprilTag + detections = AprilTagDetectorJNI.runAprilTagDetectorSimple(nativeApriltagPtr, grey, tagsize, fx, fy, cx, cy); + + synchronized (detectionsUpdateSync) + { + detectionsUpdate = detections; + } + + // For fun, use OpenCV to draw 6DOF markers on the image. We actually recompute the pose using + // OpenCV because I haven't yet figured out how to re-use AprilTag's pose in OpenCV. + for(AprilTagDetection detection : detections) + { + Pose pose = poseFromTrapezoid(detection.corners, cameraMatrix, tagsizeX, tagsizeY); + drawAxisMarker(input, tagsizeY/2.0, 6, pose.rvec, pose.tvec, cameraMatrix); + draw3dCubeMarker(input, tagsizeX, tagsizeX, tagsizeY, 5, pose.rvec, pose.tvec, cameraMatrix); + } + + return input; + } + + public void setDecimation(float decimation) + { + synchronized (decimationSync) + { + this.decimation = decimation; + needToSetDecimation = true; + } + } + + public ArrayList getLatestDetections() + { + return detections; + } + + public ArrayList getDetectionsUpdate() + { + synchronized (detectionsUpdateSync) + { + ArrayList ret = detectionsUpdate; + detectionsUpdate = null; + return ret; + } + } + + void constructMatrix() + { + // Construct the camera matrix. + // + // -- -- + // | fx 0 cx | + // | 0 fy cy | + // | 0 0 1 | + // -- -- + // + + cameraMatrix = new Mat(3,3, CvType.CV_32FC1); + + cameraMatrix.put(0,0, fx); + cameraMatrix.put(0,1,0); + cameraMatrix.put(0,2, cx); + + cameraMatrix.put(1,0,0); + cameraMatrix.put(1,1,fy); + cameraMatrix.put(1,2,cy); + + cameraMatrix.put(2, 0, 0); + cameraMatrix.put(2,1,0); + cameraMatrix.put(2,2,1); + } + + /** + * Draw a 3D axis marker on a detection. (Similar to what Vuforia does) + * + * @param buf the RGB buffer on which to draw the marker + * @param length the length of each of the marker 'poles' + * @param rvec the rotation vector of the detection + * @param tvec the translation vector of the detection + * @param cameraMatrix the camera matrix used when finding the detection + */ + void drawAxisMarker(Mat buf, double length, int thickness, Mat rvec, Mat tvec, Mat cameraMatrix) + { + // The points in 3D space we wish to project onto the 2D image plane. + // The origin of the coordinate space is assumed to be in the center of the detection. + MatOfPoint3f axis = new MatOfPoint3f( + new Point3(0,0,0), + new Point3(length,0,0), + new Point3(0,length,0), + new Point3(0,0,-length) + ); + + // Project those points + MatOfPoint2f matProjectedPoints = new MatOfPoint2f(); + Calib3d.projectPoints(axis, rvec, tvec, cameraMatrix, new MatOfDouble(), matProjectedPoints); + Point[] projectedPoints = matProjectedPoints.toArray(); + + // Draw the marker! + Imgproc.line(buf, projectedPoints[0], projectedPoints[1], red, thickness); + Imgproc.line(buf, projectedPoints[0], projectedPoints[2], green, thickness); + Imgproc.line(buf, projectedPoints[0], projectedPoints[3], blue, thickness); + + Imgproc.circle(buf, projectedPoints[0], thickness, white, -1); + } + + void draw3dCubeMarker(Mat buf, double length, double tagWidth, double tagHeight, int thickness, Mat rvec, Mat tvec, Mat cameraMatrix) + { + //axis = np.float32([[0,0,0], [0,3,0], [3,3,0], [3,0,0], + // [0,0,-3],[0,3,-3],[3,3,-3],[3,0,-3] ]) + + // The points in 3D space we wish to project onto the 2D image plane. + // The origin of the coordinate space is assumed to be in the center of the detection. + MatOfPoint3f axis = new MatOfPoint3f( + new Point3(-tagWidth/2, tagHeight/2,0), + new Point3( tagWidth/2, tagHeight/2,0), + new Point3( tagWidth/2,-tagHeight/2,0), + new Point3(-tagWidth/2,-tagHeight/2,0), + new Point3(-tagWidth/2, tagHeight/2,-length), + new Point3( tagWidth/2, tagHeight/2,-length), + new Point3( tagWidth/2,-tagHeight/2,-length), + new Point3(-tagWidth/2,-tagHeight/2,-length)); + + // Project those points + MatOfPoint2f matProjectedPoints = new MatOfPoint2f(); + Calib3d.projectPoints(axis, rvec, tvec, cameraMatrix, new MatOfDouble(), matProjectedPoints); + Point[] projectedPoints = matProjectedPoints.toArray(); + + // Pillars + for(int i = 0; i < 4; i++) + { + Imgproc.line(buf, projectedPoints[i], projectedPoints[i+4], blue, thickness); + } + + // Base lines + //Imgproc.line(buf, projectedPoints[0], projectedPoints[1], blue, thickness); + //Imgproc.line(buf, projectedPoints[1], projectedPoints[2], blue, thickness); + //Imgproc.line(buf, projectedPoints[2], projectedPoints[3], blue, thickness); + //Imgproc.line(buf, projectedPoints[3], projectedPoints[0], blue, thickness); + + // Top lines + Imgproc.line(buf, projectedPoints[4], projectedPoints[5], green, thickness); + Imgproc.line(buf, projectedPoints[5], projectedPoints[6], green, thickness); + Imgproc.line(buf, projectedPoints[6], projectedPoints[7], green, thickness); + Imgproc.line(buf, projectedPoints[4], projectedPoints[7], green, thickness); + } + + /** + * Extracts 6DOF pose from a trapezoid, using a camera intrinsics matrix and the + * original size of the tag. + * + * @param points the points which form the trapezoid + * @param cameraMatrix the camera intrinsics matrix + * @param tagsizeX the original width of the tag + * @param tagsizeY the original height of the tag + * @return the 6DOF pose of the camera relative to the tag + */ + Pose poseFromTrapezoid(Point[] points, Mat cameraMatrix, double tagsizeX , double tagsizeY) + { + // The actual 2d points of the tag detected in the image + MatOfPoint2f points2d = new MatOfPoint2f(points); + + // The 3d points of the tag in an 'ideal projection' + Point3[] arrayPoints3d = new Point3[4]; + arrayPoints3d[0] = new Point3(-tagsizeX/2, tagsizeY/2, 0); + arrayPoints3d[1] = new Point3(tagsizeX/2, tagsizeY/2, 0); + arrayPoints3d[2] = new Point3(tagsizeX/2, -tagsizeY/2, 0); + arrayPoints3d[3] = new Point3(-tagsizeX/2, -tagsizeY/2, 0); + MatOfPoint3f points3d = new MatOfPoint3f(arrayPoints3d); + + // Using this information, actually solve for pose + Pose pose = new Pose(); + Calib3d.solvePnP(points3d, points2d, cameraMatrix, new MatOfDouble(), pose.rvec, pose.tvec, false); + + return pose; + } + + /* + * A simple container to hold both rotation and translation + * vectors, which together form a 6DOF pose. + */ + class Pose + { + Mat rvec; + Mat tvec; + + public Pose() + { + rvec = new Mat(); + tvec = new Mat(); + } + + public Pose(Mat rvec, Mat tvec) + { + this.rvec = rvec; + this.tvec = tvec; + } + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index b873b758..6fd3d7a8 100644 --- a/build.gradle +++ b/build.gradle @@ -7,6 +7,7 @@ buildscript { kotlin_version = "1.5.10" kotlinx_coroutines_version = "1.5.0-native-mt" opencv_version = "4.5.1-2" + apriltag_plugin_version = "1.0.0" env = findProperty('env') == 'release' ? 'release' : 'dev' @@ -23,7 +24,7 @@ buildscript { } allprojects { - group 'com.github.serivesmejia' + group 'com.github.deltacv' version '3.2.0' ext { @@ -32,6 +33,8 @@ allprojects { repositories { mavenCentral() + mavenLocal() + maven { url "https://jitpack.io" } } tasks.withType(Jar) { From 627a00870d8befa0c3ee96020dbc9328ce13291e Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Fri, 1 Oct 2021 20:45:02 -0600 Subject: [PATCH 51/56] Add classpath scanning for nodes & coroutines --- EasyVision/build.gradle | 4 ++ .../github/deltacv/easyvision/EasyVision.kt | 27 ++++------ .../github/deltacv/easyvision/NodeManager.kt | 7 --- .../easyvision/codegen/CodeGenManager.kt | 18 +++++++ .../easyvision/{ => node}/NodeEditor.kt | 12 +++-- .../deltacv/easyvision/{ => node}/NodeList.kt | 22 ++++++-- .../deltacv/easyvision/node/NodeScanner.kt | 53 +++++++++++++++++++ 7 files changed, 111 insertions(+), 32 deletions(-) delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/NodeManager.kt create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGenManager.kt rename EasyVision/src/main/kotlin/io/github/deltacv/easyvision/{ => node}/NodeEditor.kt (89%) rename EasyVision/src/main/kotlin/io/github/deltacv/easyvision/{ => node}/NodeList.kt (89%) create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeScanner.kt diff --git a/EasyVision/build.gradle b/EasyVision/build.gradle index 32bd5b38..78d64475 100644 --- a/EasyVision/build.gradle +++ b/EasyVision/build.gradle @@ -9,6 +9,10 @@ version = "1.0.0" dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_coroutines_version" + implementation "io.github.spair:imgui-java-app:1.84.1.0" + implementation 'com.google.code.gson:gson:2.8.7' + implementation 'io.github.classgraph:classgraph:4.8.108' } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt index c3dd62d4..f71247fb 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt @@ -33,8 +33,9 @@ import io.github.deltacv.easyvision.codegen.* import io.github.deltacv.easyvision.gui.PopupBuilder import io.github.deltacv.easyvision.gui.makeFont import io.github.deltacv.easyvision.id.IdElementContainer +import io.github.deltacv.easyvision.node.NodeEditor +import io.github.deltacv.easyvision.node.NodeList import io.github.deltacv.easyvision.node.vision.* -import io.github.deltacv.easyvision.util.ElapsedTime import org.lwjgl.BufferUtils import org.lwjgl.glfw.GLFW import org.lwjgl.glfw.GLFW.* @@ -62,23 +63,19 @@ class EasyVision : Application() { private var prevKeyCallback: GLFWKeyCallback? = null - val editor = NodeEditor(this) + val nodeEditor = NodeEditor(this) val nodeList = NodeList(this) + val codeGenManager = CodeGenManager(this) + private lateinit var defaultFont: ImFont - - private val inputNode = InputMatNode() - private val outputNode = OutputMatNode() fun start() { - editor.init() - - inputNode.enable() - outputNode.enable() + nodeEditor.init() launch(this) - editor.destroy() + nodeEditor.destroy() } override fun configure(config: Configuration) { @@ -111,7 +108,7 @@ class EasyVision : Application() { or ImGuiWindowFlags.NoTitleBar or ImGuiWindowFlags.NoDecoration ) - editor.draw() + nodeEditor.draw() ImGui.end() ImGui.popFont() @@ -127,13 +124,7 @@ class EasyVision : Application() { isSpaceReleased = false if(ImGui.isMouseReleased(ImGuiMouseButton.Right)) { - val timer = ElapsedTime() - - val codeGen = CodeGen("TestPipeline") - inputNode.startGen(codeGen.currScopeProcessFrame) - - println(codeGen.gen()) - println("took ${timer.seconds}") + codeGenManager.build() } } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/NodeManager.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/NodeManager.kt deleted file mode 100644 index 1de5dfb8..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/NodeManager.kt +++ /dev/null @@ -1,7 +0,0 @@ -package io.github.deltacv.easyvision - -class NodeManager { - - - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGenManager.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGenManager.kt new file mode 100644 index 00000000..07ddb262 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGenManager.kt @@ -0,0 +1,18 @@ +package io.github.deltacv.easyvision.codegen + +import io.github.deltacv.easyvision.EasyVision +import io.github.deltacv.easyvision.util.ElapsedTime + +class CodeGenManager(val easyVision: EasyVision) { + + fun build() { + val timer = ElapsedTime() + + val codeGen = CodeGen("TestPipeline") + easyVision.nodeEditor.inputNode.startGen(codeGen.currScopeProcessFrame) + + println(codeGen.gen()) + println("took ${timer.seconds}") + } + +} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/NodeEditor.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt similarity index 89% rename from EasyVision/src/main/kotlin/io/github/deltacv/easyvision/NodeEditor.kt rename to EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt index 018928d6..a2bb55ec 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/NodeEditor.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt @@ -1,14 +1,15 @@ -package io.github.deltacv.easyvision +package io.github.deltacv.easyvision.node import imgui.ImGui import imgui.extension.imnodes.ImNodes import imgui.extension.imnodes.ImNodesContext import imgui.flag.ImGuiMouseButton import imgui.type.ImInt +import io.github.deltacv.easyvision.EasyVision import io.github.deltacv.easyvision.gui.PopupBuilder import io.github.deltacv.easyvision.attribute.AttributeMode -import io.github.deltacv.easyvision.node.Link -import io.github.deltacv.easyvision.node.Node +import io.github.deltacv.easyvision.node.vision.InputMatNode +import io.github.deltacv.easyvision.node.vision.OutputMatNode class NodeEditor(val easyVision: EasyVision) { @@ -17,8 +18,13 @@ class NodeEditor(val easyVision: EasyVision) { var isNodeFocused = false private set + val inputNode = InputMatNode() + val outputNode = OutputMatNode() + fun init() { ImNodes.createContext() + inputNode.enable() + outputNode.enable() } fun draw() { diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/NodeList.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt similarity index 89% rename from EasyVision/src/main/kotlin/io/github/deltacv/easyvision/NodeList.kt rename to EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt index df519f39..8c9bd0ea 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/NodeList.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt @@ -1,4 +1,4 @@ -package io.github.deltacv.easyvision +package io.github.deltacv.easyvision.node import imgui.ImColor import imgui.ImFont @@ -11,19 +11,27 @@ import imgui.flag.ImGuiCol import imgui.flag.ImGuiCond import imgui.flag.ImGuiMouseButton import imgui.flag.ImGuiWindowFlags +import io.github.deltacv.easyvision.EasyVision import io.github.deltacv.easyvision.attribute.Attribute import io.github.deltacv.easyvision.gui.makeFont import io.github.deltacv.easyvision.id.IdElementContainer -import io.github.deltacv.easyvision.node.DrawNode -import io.github.deltacv.easyvision.node.Node import io.github.deltacv.easyvision.node.vision.CvtColorNode import io.github.deltacv.easyvision.util.ElapsedTime +import kotlinx.coroutines.* class NodeList(val easyVision: EasyVision) { companion object { val listNodes = IdElementContainer>() val listAttributes = IdElementContainer() + + lateinit var annotatedNodes: List>> + private set + + @OptIn(DelicateCoroutinesApi::class) + private val annotatedNodesJob = GlobalScope.launch(Dispatchers.IO) { + annotatedNodes = NodeScanner.scan() + } } lateinit var buttonFont: ImFont @@ -52,9 +60,15 @@ class NodeList(val easyVision: EasyVision) { } fun draw() { + if(!annotatedNodesJob.isCompleted) { + runBlocking { + annotatedNodesJob.join() + } + } + val size = EasyVision.windowSize - if(!easyVision.editor.isNodeFocused && easyVision.isSpaceReleased) { + if(!easyVision.nodeEditor.isNodeFocused && easyVision.isSpaceReleased) { showList() } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeScanner.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeScanner.kt new file mode 100644 index 00000000..fa2cf763 --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeScanner.kt @@ -0,0 +1,53 @@ +package io.github.deltacv.easyvision.node + +import io.github.classgraph.ClassGraph +import io.github.deltacv.easyvision.util.ElapsedTime +import kotlinx.coroutines.* + +object NodeScanner { + + val ignoredPackages = arrayOf( + "java", + "org.opencv", + "imgui", + "io.github.classgraph", + "org.lwjgl" + ) + + @Suppress("UNCHECKED_CAST") //shut + fun scan(): List>> { + val nodesClasses = mutableListOf>>() + + println("Scanning for nodes...") + + val classGraph = ClassGraph() + .enableClassInfo() + .enableAnnotationInfo() + .rejectPackages(*ignoredPackages) + + val scanResult = classGraph.scan() + val nodeClasses = scanResult.getClassesWithAnnotation(AddNode::class.java.name) + + for(nodeClass in nodeClasses) { + val clazz = Class.forName(nodeClass.name) + + if(hasSuperclass(clazz, Node::class.java)) { + nodesClasses.add(clazz as Class>) + } + } + + println("Found ${nodesClasses.size} nodes") + + return nodesClasses + } + +} + +fun hasSuperclass(clazz: Class<*>, superClass: Class<*>): Boolean { + return try { + clazz.asSubclass(superClass) + true + } catch (ex: ClassCastException) { + false + } +} \ No newline at end of file From efe5e1628fbb256ea40d39e9d329dd6b2ea0ded7 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Sat, 2 Oct 2021 01:38:56 -0600 Subject: [PATCH 52/56] Improved error handling for pipeline initialization exceptions --- .../eocvsim/pipeline/PipelineManager.kt | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt index a97e4062..746ecb44 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt @@ -30,6 +30,7 @@ import com.github.serivesmejia.eocvsim.pipeline.compiler.CompiledPipelineManager import com.github.serivesmejia.eocvsim.pipeline.util.PipelineExceptionTracker import com.github.serivesmejia.eocvsim.pipeline.util.PipelineSnapshot import com.github.serivesmejia.eocvsim.util.Log +import com.github.serivesmejia.eocvsim.util.StrUtil import com.github.serivesmejia.eocvsim.util.event.EventHandler import com.github.serivesmejia.eocvsim.util.exception.MaxActiveContextsException import com.github.serivesmejia.eocvsim.util.fps.FpsCounter @@ -78,6 +79,7 @@ class PipelineManager(var eocvSim: EOCVSim) { private set var currentPipelineIndex = -1 private set + var previousPipelineIndex = 0 val activePipelineContexts = ArrayList() private var currentPipelineContext: ExecutorCoroutineDispatcher? = null @@ -256,7 +258,22 @@ class PipelineManager(var eocvSim: EOCVSim) { updateExceptionTracker() } catch (ex: Exception) { //handling exceptions from pipelines - updateExceptionTracker(ex) + if(!hasInitCurrentPipeline) { + pipelineExceptionTracker.addMessage("Error while initializing requested pipeline, \"$currentPipelineName\". Falling back to previous one.") + pipelineExceptionTracker.addMessage( + StrUtil.cutStringBy( + StrUtil.fromException(ex), "\n", 9 + ).trim() + ) + + eocvSim.visualizer.pipelineSelectorPanel.selectedIndex = previousPipelineIndex + changePipeline(currentPipelineIndex) + + Log.error(TAG, "Error while initializing requested pipeline, $currentPipelineName", ex) + Log.blank() + } else { + updateExceptionTracker(ex) + } } } @@ -356,6 +373,7 @@ class PipelineManager(var eocvSim: EOCVSim) { Log.warn(TAG, "Error while adding pipeline class", ex) Log.warn(TAG, "Unable to cast " + C.name + " to OpenCvPipeline class.") Log.warn(TAG, "Remember that the pipeline class should extend OpenCvPipeline") + updateExceptionTracker(ex) } } @@ -460,6 +478,8 @@ class PipelineManager(var eocvSim: EOCVSim) { return } + previousPipelineIndex = currentPipelineIndex + currentPipeline = nextPipeline currentPipelineData = pipelines[index] currentTelemetry = nextTelemetry From ce7d70075f17c66e6321f8ac4e777d673e432f3a Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Sat, 2 Oct 2021 23:14:46 -0600 Subject: [PATCH 53/56] Add collapsing headers to nodes list --- .../github/deltacv/easyvision/node/AddNode.kt | 5 --- .../deltacv/easyvision/node/NodeList.kt | 39 +++++++++++++------ .../deltacv/easyvision/node/NodeScanner.kt | 4 +- .../deltacv/easyvision/node/RegisterNode.kt | 9 +++++ .../easyvision/node/math/SumIntegerNode.kt | 4 +- .../easyvision/node/vision/CvtColorNode.kt | 4 +- .../easyvision/node/vision/MaskNode.kt | 4 +- .../easyvision/node/vision/ThresholdNode.kt | 4 +- 8 files changed, 45 insertions(+), 28 deletions(-) delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/AddNode.kt create mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/RegisterNode.kt diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/AddNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/AddNode.kt deleted file mode 100644 index b4b94f50..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/AddNode.kt +++ /dev/null @@ -1,5 +0,0 @@ -package io.github.deltacv.easyvision.node - -enum class Category { CV_BASICS, MATH, MISC} - -annotation class AddNode(val name: String, val category: Category, val description: String = "") diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt index 8c9bd0ea..fe2c6862 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt @@ -6,11 +6,11 @@ import imgui.ImGui import imgui.ImVec2 import imgui.extension.imnodes.ImNodes import imgui.extension.imnodes.ImNodesContext +import imgui.extension.imnodes.flag.ImNodesAttributeFlags import imgui.extension.imnodes.flag.ImNodesColorStyle -import imgui.flag.ImGuiCol -import imgui.flag.ImGuiCond -import imgui.flag.ImGuiMouseButton -import imgui.flag.ImGuiWindowFlags +import imgui.extension.imnodes.flag.ImNodesStyleFlags +import imgui.flag.* +import imgui.type.ImBoolean import io.github.deltacv.easyvision.EasyVision import io.github.deltacv.easyvision.attribute.Attribute import io.github.deltacv.easyvision.gui.makeFont @@ -117,7 +117,7 @@ class NodeList(val easyVision: EasyVision) { ImGui.popFont() - if (button != lastButton && button && !isNodesListOpen && openButtonTimeout.millis > 200) { + if (button != lastButton && !isNodesListOpen && button && openButtonTimeout.millis > 200) { showList() } @@ -148,20 +148,33 @@ class NodeList(val easyVision: EasyVision) { ImNodes.clearNodeSelection() ImNodes.clearLinkSelection() + ImNodes.editorResetPanning(0f, 0f) + + var closeOnClick = true ImNodes.beginNodeEditor() - for(node in listNodes) { - node.draw() + val flags = ImGuiTreeNodeFlags.DefaultOpen + + for(category in Category.values()) { + if(ImGui.collapsingHeader(category.properName, flags)) { + if(ImGui.isItemHovered()) { + closeOnClick = false + } + + + } else if(ImGui.isItemHovered()) { + closeOnClick = false + } } ImNodes.endNodeEditor() ImNodes.getStyle().gridSpacing = 32f // back to normal ImNodes.popColorStyle() - handleClick() + handleClick(closeOnClick) } - fun handleClick() { + fun handleClick(closeOnClick: Boolean) { if(ImGui.isMouseClicked(ImGuiMouseButton.Left)) { val hovered = ImNodes.getHoveredNode() @@ -182,10 +195,11 @@ class NodeList(val easyVision: EasyVision) { instance.nextNodePosition = ImVec2(newPosX, newPosY) instance.pinToMouse = true } - } - closeList() - openButtonTimeout.reset() + closeList() + } else if(closeOnClick) { + closeList() + } } } @@ -203,6 +217,7 @@ class NodeList(val easyVision: EasyVision) { fun closeList() { if(isNodesListOpen) { isNodesListOpen = false + openButtonTimeout.reset() } } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeScanner.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeScanner.kt index fa2cf763..f8f714ae 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeScanner.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeScanner.kt @@ -1,8 +1,6 @@ package io.github.deltacv.easyvision.node import io.github.classgraph.ClassGraph -import io.github.deltacv.easyvision.util.ElapsedTime -import kotlinx.coroutines.* object NodeScanner { @@ -26,7 +24,7 @@ object NodeScanner { .rejectPackages(*ignoredPackages) val scanResult = classGraph.scan() - val nodeClasses = scanResult.getClassesWithAnnotation(AddNode::class.java.name) + val nodeClasses = scanResult.getClassesWithAnnotation(RegisterNode::class.java.name) for(nodeClass in nodeClasses) { val clazz = Class.forName(nodeClass.name) diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/RegisterNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/RegisterNode.kt new file mode 100644 index 00000000..60a839ea --- /dev/null +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/RegisterNode.kt @@ -0,0 +1,9 @@ +package io.github.deltacv.easyvision.node + +enum class Category(val properName: String) { + CV_BASICS("Basic OpenCV Operations"), + MATH("Math Operations"), + MISC("Miscellaneous") +} + +annotation class RegisterNode(val name: String, val category: Category, val description: String = "") diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt index 79d39679..29901e67 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt @@ -7,10 +7,10 @@ import io.github.deltacv.easyvision.attribute.misc.ListAttribute import io.github.deltacv.easyvision.codegen.CodeGen import io.github.deltacv.easyvision.codegen.CodeGenSession import io.github.deltacv.easyvision.codegen.GenValue -import io.github.deltacv.easyvision.node.AddNode +import io.github.deltacv.easyvision.node.RegisterNode import io.github.deltacv.easyvision.node.Category -@AddNode( +@RegisterNode( name = "Sum Integers", category = Category.MATH, description = "Sums a list of integers and outputs the result" diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt index d5bc4d62..2816a973 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt @@ -6,7 +6,7 @@ import io.github.deltacv.easyvision.attribute.vision.MatAttribute import io.github.deltacv.easyvision.codegen.* import io.github.deltacv.easyvision.codegen.CodeGenSession import io.github.deltacv.easyvision.codegen.parse.* -import io.github.deltacv.easyvision.node.AddNode +import io.github.deltacv.easyvision.node.RegisterNode import io.github.deltacv.easyvision.node.Category import io.github.deltacv.easyvision.node.DrawNode @@ -20,7 +20,7 @@ enum class Colors(val channels: Int, val channelNames: Array) { GRAY(1, arrayOf("Gray")) } -@AddNode( +@RegisterNode( name = "Convert Color", category = Category.CV_BASICS, description = "Converts a Mat from its current color space to the specified color space. If the mat is already in the specified color space, no conversion is made." diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MaskNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MaskNode.kt index 99588a36..472ae7da 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MaskNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MaskNode.kt @@ -7,11 +7,11 @@ import io.github.deltacv.easyvision.codegen.CodeGenSession import io.github.deltacv.easyvision.codegen.GenValue import io.github.deltacv.easyvision.codegen.parse.new import io.github.deltacv.easyvision.codegen.parse.v -import io.github.deltacv.easyvision.node.AddNode +import io.github.deltacv.easyvision.node.RegisterNode import io.github.deltacv.easyvision.node.Category import io.github.deltacv.easyvision.node.DrawNode -@AddNode( +@RegisterNode( name = "Binary Mask", category = Category.CV_BASICS, description = "Takes a normal image and performs a mask based on a binary image, discards or includes areas from the normal image based on the binary image." diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt index b5306554..1228bb42 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt @@ -12,11 +12,11 @@ import io.github.deltacv.easyvision.codegen.parse.cvtColorValue import io.github.deltacv.easyvision.codegen.parse.new import io.github.deltacv.easyvision.codegen.parse.v import io.github.deltacv.easyvision.gui.ExtraWidgets -import io.github.deltacv.easyvision.node.AddNode +import io.github.deltacv.easyvision.node.RegisterNode import io.github.deltacv.easyvision.node.Category import io.github.deltacv.easyvision.node.DrawNode -@AddNode( +@RegisterNode( name = "Color Threshold", category = Category.CV_BASICS, description = "Performs a threshold in the input image and returns a binary image, discarding the pixels that were outside the range in the color space specified." From 78adf2d33f18fd6923a8a774e9cf113eef6f203a Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Mon, 4 Oct 2021 09:39:09 -0600 Subject: [PATCH 54/56] Improvements to the about dialog & node scanner --- .../tuner/TunableFieldPanelConfig.kt | 1 + .../eocvsim/gui/dialog/About.java | 2 +- .../serivesmejia/eocvsim/gui/dialog/Output.kt | 2 + .../serivesmejia/eocvsim/util/SysUtil.java | 2 +- .../eocvsim/workspace/util/VSCodeLauncher.kt | 2 + .../util/template/GradleWorkspaceTemplate.kt | 14 +++-- EOCV-Sim/src/main/resources/contributors.txt | 2 +- .../src/main/resources/opensourcelibs.txt | 12 +++- .../resources/templates/gradle_workspace.zip | Bin 61335 -> 61361 bytes .../deltacv/easyvision/node/NodeList.kt | 51 +++++++++++------ .../deltacv/easyvision/node/NodeScanner.kt | 23 ++++++-- .../teamcode/AprilTagDetectionPipeline.java | 54 ++++++++++++------ 12 files changed, 116 insertions(+), 49 deletions(-) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelConfig.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelConfig.kt index 5d9b95d6..3b4b37a2 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelConfig.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/component/tuner/TunableFieldPanelConfig.kt @@ -38,6 +38,7 @@ import java.awt.GridBagLayout import javax.swing.* import javax.swing.border.EmptyBorder +@OptIn(DelicateCoroutinesApi::class) class TunableFieldPanelConfig(private val fieldOptions: TunableFieldPanelOptions, private val eocvSim: EOCVSim) : JPanel() { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/About.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/About.java index 1b57d7e6..65b36e24 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/About.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/About.java @@ -67,7 +67,7 @@ private void initAbout() { about.setModal(true); about.setTitle("About"); - about.setSize(445, 290); + about.setSize(490, 320); JPanel contents = new JPanel(new GridLayout(2, 1)); contents.setAlignmentX(Component.CENTER_ALIGNMENT); diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/Output.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/Output.kt index d28b01ef..bf6a0cec 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/Output.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/gui/dialog/Output.kt @@ -26,6 +26,7 @@ package com.github.serivesmejia.eocvsim.gui.dialog import com.github.serivesmejia.eocvsim.EOCVSim import com.github.serivesmejia.eocvsim.gui.dialog.component.OutputPanel import com.github.serivesmejia.eocvsim.pipeline.compiler.PipelineCompileStatus +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -99,6 +100,7 @@ class Output @JvmOverloads constructor( output.isVisible = true } + @OptIn(DelicateCoroutinesApi::class) private fun registerListeners() = GlobalScope.launch(Dispatchers.Swing) { output.addWindowListener(object: WindowAdapter() { override fun windowClosing(e: WindowEvent) { diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/SysUtil.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/SysUtil.java index 884e6545..35917838 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/SysUtil.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/SysUtil.java @@ -206,7 +206,7 @@ public static boolean saveFileStr(File f, String contents) { fw.close(); return true; } catch (IOException e) { - e.printStackTrace(); + Log.error("Exception while trying to save file " + f.getAbsolutePath(), e); return false; } } diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/VSCodeLauncher.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/VSCodeLauncher.kt index 06f6a8ff..d184f9c1 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/VSCodeLauncher.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/VSCodeLauncher.kt @@ -29,6 +29,7 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import com.github.serivesmejia.eocvsim.util.SysUtil +import kotlinx.coroutines.DelicateCoroutinesApi import java.io.File object VSCodeLauncher { @@ -48,6 +49,7 @@ object VSCodeLauncher { Log.info(TAG, "VS Code failed to open") } + @OptIn(DelicateCoroutinesApi::class) fun asyncLaunch(workspace: File) = GlobalScope.launch(Dispatchers.IO) { launch(workspace) } } \ No newline at end of file diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/template/GradleWorkspaceTemplate.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/template/GradleWorkspaceTemplate.kt index 0d8fd93a..a049e1f8 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/template/GradleWorkspaceTemplate.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/workspace/util/template/GradleWorkspaceTemplate.kt @@ -23,12 +23,10 @@ package com.github.serivesmejia.eocvsim.workspace.util.template -import com.github.serivesmejia.eocvsim.util.Log import com.github.serivesmejia.eocvsim.Build +import com.github.serivesmejia.eocvsim.util.Log import com.github.serivesmejia.eocvsim.util.SysUtil -import com.github.serivesmejia.eocvsim.workspace.util.VSCodeLauncher import com.github.serivesmejia.eocvsim.workspace.util.WorkspaceTemplate -import com.github.serivesmejia.eocvsim.EOCVSim import net.lingala.zip4j.ZipFile import java.io.File import java.io.IOException @@ -67,8 +65,14 @@ object GradleWorkspaceTemplate : WorkspaceTemplate() { //replace the root project name variable in the file to the root folder name SysUtil.replaceStrInFile(settingsGradleFile, "\$workspace_name", folder.name) - //replace the version of the eocvsim dependency in build.gradle to the current one - SysUtil.replaceStrInFile(buildGradleFile, "\$version", Build.standardVersionString) + // replace the versions of the eocvsim dependencies in build.gradle to the current ones + // based on the autogenerated Build.java file + val fileContents = SysUtil.loadFileStr(buildGradleFile) + .replace("\$eocvsim_version", Build.standardVersionString) + .replace("\$opencv_version", Build.opencvVersion) + .replace("\$apriltag_version", Build.apriltagPluginVersion) + + SysUtil.saveFileStr(buildGradleFile, fileContents) } } diff --git a/EOCV-Sim/src/main/resources/contributors.txt b/EOCV-Sim/src/main/resources/contributors.txt index 8823008b..52c2729e 100644 --- a/EOCV-Sim/src/main/resources/contributors.txt +++ b/EOCV-Sim/src/main/resources/contributors.txt @@ -1,4 +1,4 @@ -NPE & the OpenFTC Team - EOCV Developers & Advisors +NPE (Windwoes) - EasyOpenCV and AprilTag Plugin serivesmejia - Main Dev Purav - Contributor & Mac Tester Jaran - Kotlin & Coroutines Advisor diff --git a/EOCV-Sim/src/main/resources/opensourcelibs.txt b/EOCV-Sim/src/main/resources/opensourcelibs.txt index c3feac3f..c02ffecc 100644 --- a/EOCV-Sim/src/main/resources/opensourcelibs.txt +++ b/EOCV-Sim/src/main/resources/opensourcelibs.txt @@ -1,8 +1,16 @@ +EOCV-Sim and its source code is distributed under the MIT License + OpenCV - Under Apache 2.0 License +OpenCV for Desktop Java - Under Apache 2.0 License FTC SDK - Some source code under the BSD License EasyOpenCV - Some source code under MIT License +EOCV-AprilTag-Plugin - Source code under MIT License +webcam-capture - Under MIT License Gson - Under Apache 2.0 License ClassGraph - Under MIT License FlatLaf - Under Apache 2.0 License - -EOCV-Sim and its source code is distributed under the MIT License \ No newline at end of file +Kotlin stdlib & coroutines - Under Apache 2.0 License +picocli - Under Apache 2.0 License +dear imgui - Under MIT License +imgui-java - Under Apache 2.0 License +LWJGL - Under BSD 3-Clause License \ No newline at end of file diff --git a/EOCV-Sim/src/main/resources/templates/gradle_workspace.zip b/EOCV-Sim/src/main/resources/templates/gradle_workspace.zip index a0c577091640502f80a119913f7369a42ef62baa..207a505c9fd68d1a6076b426ce592b21e22d8c34 100644 GIT binary patch delta 528 zcmbP!pLyeb<_#h2^;3PEgW1IvyB=gVwJg`yFIG2dj8uQaX#x3xtEqOqxP^z4D-{hbB_~# zT}*l-D0QQ7N6%pk5s`IEVmR_=vhaSE+bq_>-!FZB&e^C%8`(n>SLSWEp2YRzvQN(0 zr~Eq>B*Yo~=AAIf#YdLIYfkUiInNxbBKALLUEGxa?rZMJ`uH{WkNr zeUtZ$o#vimAPmX?_z$Cyq+2D=BVp9?0ZwA zJ7r0vhx06sgX(AOl>XR+i51B1kXd^3U6|C|mm<8W&gT?6x5@e#vag&cW4rjY_zJ~_ z%MY$_b(l$P5Y;fQa-aEW+V>glFAti|&OJ652edCy{!>K&^*K0qfv>TJ>+|Zg_ z^2U!*b@Jgi5sb!@W!{?c$TJ|hGVZN7E6~l~pG~fMD=T(uZrhp7PS+V>G>() val listAttributes = IdElementContainer() - lateinit var annotatedNodes: List>> + lateinit var categorizedNodes: CategorizedNodes private set @OptIn(DelicateCoroutinesApi::class) private val annotatedNodesJob = GlobalScope.launch(Dispatchers.IO) { - annotatedNodes = NodeScanner.scan() + categorizedNodes = NodeScanner.scan() } } @@ -48,24 +48,12 @@ class NodeList(val easyVision: EasyVision) { private lateinit var listContext: ImNodesContext - val testNode = CvtColorNode() fun init() { buttonFont = makeFont(plusFontSize) - - testNode.nodesIdContainer = listNodes - testNode.attributesIdContainer = listAttributes - testNode.drawAttributesCircles = false - testNode.enable() } fun draw() { - if(!annotatedNodesJob.isCompleted) { - runBlocking { - annotatedNodesJob.join() - } - } - val size = EasyVision.windowSize if(!easyVision.nodeEditor.isNodeFocused && easyVision.isSpaceReleased) { @@ -140,6 +128,29 @@ class NodeList(val easyVision: EasyVision) { ImGui.end() } + val nodes by lazy { + val map = mutableMapOf>>() + + for((category, nodeClasses) in categorizedNodes) { + val list = mutableListOf>() + + for(nodeClass in nodeClasses) { + val instance = nodeClass.getConstructor().newInstance() + + instance.nodesIdContainer = listNodes + instance.attributesIdContainer = listAttributes + instance.drawAttributesCircles = false + instance.enable() + + list.add(instance) + } + + map[category] = list + } + + map + } + private fun drawNodesList() { ImNodes.editorContextSet(listContext) @@ -155,13 +166,15 @@ class NodeList(val easyVision: EasyVision) { ImNodes.beginNodeEditor() val flags = ImGuiTreeNodeFlags.DefaultOpen - for(category in Category.values()) { + for((category, nodes) in nodes) { if(ImGui.collapsingHeader(category.properName, flags)) { if(ImGui.isItemHovered()) { closeOnClick = false } - + for(node in nodes) { + node.draw() + } } else if(ImGui.isItemHovered()) { closeOnClick = false } @@ -205,6 +218,12 @@ class NodeList(val easyVision: EasyVision) { fun showList() { if(!isNodesListOpen) { + if(!annotatedNodesJob.isCompleted) { + runBlocking { + annotatedNodesJob.join() // wait for the scanning to finish + } + } + if(::listContext.isInitialized) { listContext.destroy() } diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeScanner.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeScanner.kt index f8f714ae..09fa1996 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeScanner.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeScanner.kt @@ -2,6 +2,8 @@ package io.github.deltacv.easyvision.node import io.github.classgraph.ClassGraph +typealias CategorizedNodes = Map>>> + object NodeScanner { val ignoredPackages = arrayOf( @@ -13,8 +15,8 @@ object NodeScanner { ) @Suppress("UNCHECKED_CAST") //shut - fun scan(): List>> { - val nodesClasses = mutableListOf>>() + fun scan(): CategorizedNodes { + val nodes = mutableMapOf>>>() println("Scanning for nodes...") @@ -29,14 +31,25 @@ object NodeScanner { for(nodeClass in nodeClasses) { val clazz = Class.forName(nodeClass.name) + val regAnnotation = clazz.getDeclaredAnnotation(RegisterNode::class.java) + if(hasSuperclass(clazz, Node::class.java)) { - nodesClasses.add(clazz as Class>) + val nodeClazz = clazz as Class> + + var list = nodes[regAnnotation.category] + + if(list == null) { + list = mutableListOf(nodeClazz) + nodes[regAnnotation.category] = list + } else { + list.add(nodeClazz) + } } } - println("Found ${nodesClasses.size} nodes") + println("Found ${nodeClasses.size} nodes") - return nodesClasses + return nodes } } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagDetectionPipeline.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagDetectionPipeline.java index c03a56b8..2c2e4519 100644 --- a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagDetectionPipeline.java +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/AprilTagDetectionPipeline.java @@ -21,6 +21,7 @@ package org.firstinspires.ftc.teamcode; +import org.firstinspires.ftc.robotcore.external.Telemetry; import org.opencv.calib3d.Calib3d; import org.opencv.core.CvType; import org.opencv.core.Mat; @@ -39,19 +40,14 @@ public class AprilTagDetectionPipeline extends OpenCvPipeline { - private long nativeApriltagPtr; - private Mat grey = new Mat(); - private ArrayList detections = new ArrayList<>(); - - private ArrayList detectionsUpdate = new ArrayList<>(); - private final Object detectionsUpdateSync = new Object(); + // STATIC CONSTANTS - Mat cameraMatrix; + public static Scalar blue = new Scalar(7,197,235,255); + public static Scalar red = new Scalar(255,0,0,255); + public static Scalar green = new Scalar(0,255,0,255); + public static Scalar white = new Scalar(255,255,255,255); - Scalar blue = new Scalar(7,197,235,255); - Scalar red = new Scalar(255,0,0,255); - Scalar green = new Scalar(0,255,0,255); - Scalar white = new Scalar(255,255,255,255); + static final double FEET_PER_METER = 3.28084; // Lens intrinsics // UNITS ARE PIXELS @@ -63,18 +59,30 @@ public class AprilTagDetectionPipeline extends OpenCvPipeline public static double cy = 221.506; // UNITS ARE METERS - public static double tagsize = 0.166; + public static double TAG_SIZE = 0.166; + + // instance variables + + private long nativeApriltagPtr; + private Mat grey = new Mat(); + private ArrayList detections = new ArrayList<>(); + + private ArrayList detectionsUpdate = new ArrayList<>(); + private final Object detectionsUpdateSync = new Object(); - double tagsizeX; - double tagsizeY; + Mat cameraMatrix; + + double tagsizeX = TAG_SIZE; + double tagsizeY = TAG_SIZE; private float decimation; private boolean needToSetDecimation; private final Object decimationSync = new Object(); - public AprilTagDetectionPipeline() { - this.tagsizeX = tagsize; - this.tagsizeY = tagsize; + Telemetry telemetry; + + public AprilTagDetectionPipeline(Telemetry telemetry) { + this.telemetry = telemetry; constructMatrix(); } @@ -108,7 +116,7 @@ public Mat processFrame(Mat input) } // Run AprilTag - detections = AprilTagDetectorJNI.runAprilTagDetectorSimple(nativeApriltagPtr, grey, tagsize, fx, fy, cx, cy); + detections = AprilTagDetectorJNI.runAprilTagDetectorSimple(nativeApriltagPtr, grey, TAG_SIZE, fx, fy, cx, cy); synchronized (detectionsUpdateSync) { @@ -122,8 +130,18 @@ public Mat processFrame(Mat input) Pose pose = poseFromTrapezoid(detection.corners, cameraMatrix, tagsizeX, tagsizeY); drawAxisMarker(input, tagsizeY/2.0, 6, pose.rvec, pose.tvec, cameraMatrix); draw3dCubeMarker(input, tagsizeX, tagsizeX, tagsizeY, 5, pose.rvec, pose.tvec, cameraMatrix); + + telemetry.addLine(String.format("\nDetected tag ID=%d", detection.id)); + telemetry.addLine(String.format("Translation X: %.2f feet", detection.pose.x*FEET_PER_METER)); + telemetry.addLine(String.format("Translation Y: %.2f feet", detection.pose.y*FEET_PER_METER)); + telemetry.addLine(String.format("Translation Z: %.2f feet", detection.pose.z*FEET_PER_METER)); + telemetry.addLine(String.format("Rotation Yaw: %.2f degrees", Math.toDegrees(detection.pose.yaw))); + telemetry.addLine(String.format("Rotation Pitch: %.2f degrees", Math.toDegrees(detection.pose.pitch))); + telemetry.addLine(String.format("Rotation Roll: %.2f degrees", Math.toDegrees(detection.pose.roll))); } + telemetry.update(); + return input; } From 8a52e6caba85f63eb81a127ba6797db1a090de2b Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Mon, 4 Oct 2021 13:03:25 -0600 Subject: [PATCH 55/56] Improved classpath scanning in eocv sim --- .../github/serivesmejia/eocvsim/EOCVSim.kt | 5 + .../com/github/serivesmejia/eocvsim/Main.kt | 2 +- .../eocvsim/pipeline/PipelineManager.kt | 10 +- .../eocvsim/tuner/TunerManager.java | 16 ++- .../eocvsim/util/ClasspathScan.kt | 117 ++++++++++++++++++ .../deltacv/easyvision/node/NodeList.kt | 22 ++-- 6 files changed, 152 insertions(+), 20 deletions(-) create mode 100644 EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/ClasspathScan.kt diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt index 8313e85f..7c6828d4 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/EOCVSim.kt @@ -33,6 +33,7 @@ import com.github.serivesmejia.eocvsim.output.VideoRecordingSession import com.github.serivesmejia.eocvsim.pipeline.PipelineManager import com.github.serivesmejia.eocvsim.pipeline.PipelineSource import com.github.serivesmejia.eocvsim.tuner.TunerManager +import com.github.serivesmejia.eocvsim.util.ClasspathScan import com.github.serivesmejia.eocvsim.util.FileFilters import com.github.serivesmejia.eocvsim.util.Log import com.github.serivesmejia.eocvsim.util.SysUtil @@ -101,6 +102,8 @@ class EOCVSim(val params: Parameters = Parameters()) { @JvmField val workspaceManager = WorkspaceManager(this) + val classpathScan = ClasspathScan() + val config: Config get() = configManager.config @@ -143,6 +146,8 @@ class EOCVSim(val params: Parameters = Parameters()) { loadOpenCvLib() Log.blank() + classpathScan.asyncScan() + configManager.init() //load config workspaceManager.init() diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt index 3547fc52..acaea18b 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/Main.kt @@ -2,6 +2,7 @@ package com.github.serivesmejia.eocvsim import com.github.serivesmejia.eocvsim.pipeline.PipelineSource +import com.github.serivesmejia.eocvsim.util.ClasspathScan import com.github.serivesmejia.eocvsim.util.Log import picocli.CommandLine import java.io.File @@ -27,7 +28,6 @@ class EOCVSimCommandInterface : Runnable { @CommandLine.Option(names = ["-p", "--pipeline"], description = ["Specifies the pipeline selected when the simulator starts, and the initial runtime build finishes if it was running"]) @JvmField var initialPipeline = "" - @CommandLine.Option(names = ["-s", "--source"], description = ["Specifies the source of the pipeline that will be selected when the simulator starts, from the --pipeline argument. Defaults to CLASSPATH. Possible values: \${COMPLETION-CANDIDATES}"]) @JvmField var initialPipelineSource = PipelineSource.CLASSPATH diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt index 746ecb44..0be58cbd 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/pipeline/PipelineManager.kt @@ -126,16 +126,18 @@ class PipelineManager(var eocvSim: EOCVSim) { //add default pipeline addPipelineClass(DefaultPipeline::class.java) + compiledPipelineManager.init() + + eocvSim.classpathScan.join() + //scan for pipelines - PipelineScanner(eocvSim.params.scanForPipelinesIn).lookForPipelines { - addPipelineClass(it) + for(pipelineClass in eocvSim.classpathScan.scanResult.pipelineClasses) { + addPipelineClass(pipelineClass) } Log.info(TAG, "Found " + pipelines.size + " pipeline(s)") Log.blank() - compiledPipelineManager.init() - // changing to initial pipeline onUpdate.doOnce { if(compiledPipelineManager.isBuildRunning) diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunerManager.java b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunerManager.java index 1b515f38..2df9c07d 100644 --- a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunerManager.java +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/tuner/TunerManager.java @@ -37,6 +37,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; @SuppressWarnings("rawtypes") public class TunerManager { @@ -58,12 +59,17 @@ public TunerManager(EOCVSim eocvSim) { public void init() { if(tunableFieldsTypes == null) { - AnnotatedTunableFieldScanner.ScanResult result = new AnnotatedTunableFieldScanner( - eocvSim.getParams().getScanForTunableFieldsIn() - ).scan(); + tunableFieldsTypes = new HashMap<>(); + // ... + for(Class> clazz : eocvSim.getClasspathScan().getScanResult().getTunableFieldClasses()) { + tunableFieldsTypes.put(ReflectUtil.getTypeArgumentsFrom(clazz)[0], clazz); + } + } - tunableFieldsTypes = result.getTunableFields(); - tunableFieldAcceptors = result.getAcceptors(); + if(tunableFieldAcceptors == null) { + tunableFieldAcceptors = new HashMap<>(); + // oh god... + eocvSim.getClasspathScan().getScanResult().getTunableFieldAcceptorClasses().forEach(tunableFieldAcceptors::put); } // for some reason, acceptorManager becomes null after a certain time passes diff --git a/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/ClasspathScan.kt b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/ClasspathScan.kt new file mode 100644 index 00000000..550ce527 --- /dev/null +++ b/EOCV-Sim/src/main/java/com/github/serivesmejia/eocvsim/util/ClasspathScan.kt @@ -0,0 +1,117 @@ +package com.github.serivesmejia.eocvsim.util + +import com.github.serivesmejia.eocvsim.tuner.TunableField +import com.github.serivesmejia.eocvsim.tuner.TunableFieldAcceptor +import com.github.serivesmejia.eocvsim.tuner.scanner.RegisterTunableField +import com.qualcomm.robotcore.util.ElapsedTime +import io.github.classgraph.ClassGraph +import kotlinx.coroutines.* +import org.openftc.easyopencv.OpenCvPipeline + +class ClasspathScan { + + companion object { + val TAG = "ClasspathScan" + } + + val ignoredPackages = arrayOf( + "java", + "org.opencv", + "imgui", + "io.github.classgraph", + "io.github.deltacv", + "com.github.serivesmejia.eocvsim.pipeline", + "org.openftc", + "org.lwjgl" + ) + + lateinit var scanResult: ScanResult + private set + + private lateinit var scanResultJob: Job + + @Suppress("UNCHECKED_CAST") + fun scan() { + val timer = ElapsedTime() + val classGraph = ClassGraph() + .enableClassInfo() + .enableAnnotationInfo() + .rejectPackages(*ignoredPackages) + + Log.info(TAG, "Starting to scan classpath...") + + val scanResult = classGraph.scan() + + Log.info(TAG, "ClassGraph finished scanning (took ${timer.seconds()}s)") + + val tunableFieldClassesInfo = scanResult.getClassesWithAnnotation(RegisterTunableField::class.java.name) + val pipelineClassesInfo = scanResult.getSubclasses(OpenCvPipeline::class.java.name) + + val pipelineClasses = mutableListOf>() + + for(pipelineClassInfo in pipelineClassesInfo) { + val clazz = Class.forName(pipelineClassInfo.name) + + if(ReflectUtil.hasSuperclass(clazz, OpenCvPipeline::class.java)) { + Log.info(TAG, "Found pipeline ${clazz.typeName}") + pipelineClasses.add(clazz as Class) + } + } + + Log.blank() + Log.info(TAG, "Found ${pipelineClasses.size} pipelines") + Log.blank() + + val tunableFieldClasses = mutableListOf>>() + val tunableFieldAcceptorClasses = mutableMapOf>, Class>() + + for(tunableFieldClassInfo in tunableFieldClassesInfo) { + val clazz = Class.forName(tunableFieldClassInfo.name) + + if(ReflectUtil.hasSuperclass(clazz, TunableField::class.java)) { + val tunableFieldClass = clazz as Class> + + tunableFieldClasses.add(tunableFieldClass) + Log.info(TAG, "Found tunable field ${clazz.typeName}") + + for(subclass in clazz.declaredClasses) { + if(ReflectUtil.hasSuperclass(subclass, TunableFieldAcceptor::class.java)) { + tunableFieldAcceptorClasses[tunableFieldClass] = subclass as Class + Log.info(TAG, "Found acceptor for this tunable field, ${clazz.typeName}") + break + } + } + } + } + + Log.blank() + Log.info(TAG, "Found ${tunableFieldClasses.size} tunable fields and ${tunableFieldAcceptorClasses.size} acceptors") + Log.blank() + + Log.info(TAG, "Finished scanning (took ${timer.seconds()}s)") + + this.scanResult = ScanResult( + pipelineClasses.toTypedArray(), + tunableFieldClasses.toTypedArray(), + tunableFieldAcceptorClasses.toMap() + ) + } + + @OptIn(DelicateCoroutinesApi::class) + fun asyncScan() { + scanResultJob = GlobalScope.launch(Dispatchers.IO) { + scan() + } + } + + fun join() = runBlocking { + scanResultJob.join() + } + +} + +data class ScanResult( + val pipelineClasses: Array>, + val tunableFieldClasses: Array>>, + val tunableFieldAcceptorClasses: Map>, Class> +) \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt index da6ecae5..1c1348bc 100644 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt +++ b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt @@ -166,17 +166,19 @@ class NodeList(val easyVision: EasyVision) { ImNodes.beginNodeEditor() val flags = ImGuiTreeNodeFlags.DefaultOpen - for((category, nodes) in nodes) { - if(ImGui.collapsingHeader(category.properName, flags)) { - if(ImGui.isItemHovered()) { - closeOnClick = false + for(category in Category.values()) { + if(nodes.containsKey(category)) { + for (node in nodes[category]!!) { + if (ImGui.collapsingHeader(category.properName, flags)) { + if (ImGui.isItemHovered()) { + closeOnClick = false + } + + node.draw() + } else if (ImGui.isItemHovered()) { + closeOnClick = false + } } - - for(node in nodes) { - node.draw() - } - } else if(ImGui.isItemHovered()) { - closeOnClick = false } } ImNodes.endNodeEditor() From df108d0b48ca58cf367f2af25b2d4307ba297235 Mon Sep 17 00:00:00 2001 From: serivesmejia Date: Mon, 4 Oct 2021 13:29:13 -0600 Subject: [PATCH 56/56] Preparing for release 3.2.0 --- EasyVision/build.gradle | 18 -- .../github/deltacv/easyvision/EasyVision.kt | 151 ----------- .../io/github/deltacv/easyvision/Project.kt | 4 - .../deltacv/easyvision/attribute/Attribute.kt | 135 ---------- .../easyvision/attribute/TypedAttribute.kt | 44 ---- .../attribute/math/BooleanAttribute.kt | 59 ----- .../attribute/math/IntegerAttribute.kt | 61 ----- .../attribute/misc/EnumAttribute.kt | 84 ------ .../attribute/misc/ListAttribute.kt | 155 ----------- .../attribute/vision/MatAttribute.kt | 41 --- .../attribute/vision/RangeAttribute.kt | 82 ------ .../attribute/vision/ScalarAttribute.kt | 50 ---- .../deltacv/easyvision/codegen/CodeGen.kt | 95 ------- .../easyvision/codegen/CodeGenManager.kt | 18 -- .../github/deltacv/easyvision/codegen/Csv.kt | 28 -- .../deltacv/easyvision/codegen/GenValue.kt | 52 ---- .../easyvision/codegen/dsl/CodeGenContext.kt | 54 ---- .../easyvision/codegen/dsl/ScopeContext.kt | 31 --- .../deltacv/easyvision/codegen/parse/Scope.kt | 199 -------------- .../deltacv/easyvision/codegen/parse/Value.kt | 23 -- .../easyvision/codegen/vision/MatRecycle.kt | 4 - .../easyvision/exception/GenException.kt | 8 - .../deltacv/easyvision/gui/ExtraWidgets.kt | 86 ------ .../deltacv/easyvision/gui/PopupBuilder.kt | 93 ------- .../github/deltacv/easyvision/id/IdElement.kt | 23 -- .../easyvision/id/IdElementContainer.kt | 44 ---- .../deltacv/easyvision/node/DrawNode.kt | 75 ------ .../io/github/deltacv/easyvision/node/Link.kt | 60 ----- .../io/github/deltacv/easyvision/node/Node.kt | 183 ------------- .../deltacv/easyvision/node/NodeEditor.kt | 137 ---------- .../deltacv/easyvision/node/NodeList.kt | 245 ------------------ .../deltacv/easyvision/node/NodeScanner.kt | 64 ----- .../deltacv/easyvision/node/RegisterNode.kt | 9 - .../easyvision/node/math/SumIntegerNode.kt | 50 ---- .../easyvision/node/vision/CvtColorNode.kt | 95 ------- .../easyvision/node/vision/MaskNode.kt | 72 ----- .../easyvision/node/vision/MatNodes.kt | 66 ----- .../easyvision/node/vision/ThresholdNode.kt | 145 ----------- .../serialization/NodeSerializer.kt | 4 - .../deltacv/easyvision/util/ElapsedTime.kt | 15 -- .../eocvsim_screenshot_installation_1.png | Bin 17362 -> 0 bytes .../eocvsim_screenshot_installation_2.png | Bin 4278 -> 0 bytes .../eocvsim_screenshot_installation_3.png | Bin 18310 -> 0 bytes .../eocvsim_screenshot_installation_4.png | Bin 8543 -> 0 bytes .../eocvsim_screenshot_installation_5.png | Bin 120140 -> 0 bytes doc/images/eocvsim_screenshot_structure.png | Bin 11151 -> 0 bytes doc/images/eocvsim_usage_createclass.gif | Bin 738511 -> 0 bytes doc/images/eocvsim_usage_defaultpipeline.png | Bin 4447 -> 0 bytes doc/images/eocvsim_usage_popup_teamcode.gif | Bin 516091 -> 0 bytes doc/images/eocvsim_usage_telemetry.png | Bin 2275 -> 0 bytes doc/images/eocvsim_usage_tuner_config.png | Bin 18272 -> 0 bytes .../eocvsim_usage_tuner_config_apply.png | Bin 4653 -> 0 bytes doc/images/eocvsim_usage_tuner_parts.png | Bin 3865 -> 0 bytes .../eocvsim_usage_tuner_thresholdsample_1.png | Bin 436586 -> 0 bytes .../eocvsim_usage_tuner_thresholdsample_2.png | Bin 40283 -> 0 bytes ...vsim_usage_tuner_thresholdsample_final.png | Bin 58222 -> 0 bytes doc/images/eocvsim_usage_tuneropen.png | Bin 1380 -> 0 bytes doc/images/eocvsim_usage_tunerposition.png | Bin 8264 -> 0 bytes doc/images/eocvsim_usage_workspace_select.gif | Bin 1291213 -> 0 bytes settings.gradle | 2 - 60 files changed, 2864 deletions(-) delete mode 100644 EasyVision/build.gradle delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/Project.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/BooleanAttribute.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/EnumAttribute.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/MatAttribute.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/RangeAttribute.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/ScalarAttribute.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGenManager.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Csv.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/GenValue.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/CodeGenContext.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/ScopeContext.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/parse/Scope.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/parse/Value.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/vision/MatRecycle.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/exception/GenException.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/gui/ExtraWidgets.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/gui/PopupBuilder.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/id/IdElement.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/id/IdElementContainer.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/DrawNode.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Link.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeScanner.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/RegisterNode.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MaskNode.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/serialization/NodeSerializer.kt delete mode 100644 EasyVision/src/main/kotlin/io/github/deltacv/easyvision/util/ElapsedTime.kt delete mode 100644 doc/images/eocvsim_screenshot_installation_1.png delete mode 100644 doc/images/eocvsim_screenshot_installation_2.png delete mode 100644 doc/images/eocvsim_screenshot_installation_3.png delete mode 100644 doc/images/eocvsim_screenshot_installation_4.png delete mode 100644 doc/images/eocvsim_screenshot_installation_5.png delete mode 100644 doc/images/eocvsim_screenshot_structure.png delete mode 100644 doc/images/eocvsim_usage_createclass.gif delete mode 100644 doc/images/eocvsim_usage_defaultpipeline.png delete mode 100644 doc/images/eocvsim_usage_popup_teamcode.gif delete mode 100644 doc/images/eocvsim_usage_telemetry.png delete mode 100644 doc/images/eocvsim_usage_tuner_config.png delete mode 100644 doc/images/eocvsim_usage_tuner_config_apply.png delete mode 100644 doc/images/eocvsim_usage_tuner_parts.png delete mode 100644 doc/images/eocvsim_usage_tuner_thresholdsample_1.png delete mode 100644 doc/images/eocvsim_usage_tuner_thresholdsample_2.png delete mode 100644 doc/images/eocvsim_usage_tuner_thresholdsample_final.png delete mode 100644 doc/images/eocvsim_usage_tuneropen.png delete mode 100644 doc/images/eocvsim_usage_tunerposition.png delete mode 100644 doc/images/eocvsim_usage_workspace_select.gif diff --git a/EasyVision/build.gradle b/EasyVision/build.gradle deleted file mode 100644 index 78d64475..00000000 --- a/EasyVision/build.gradle +++ /dev/null @@ -1,18 +0,0 @@ -plugins { - id 'java' - id 'org.jetbrains.kotlin.jvm' -} - -apply from: '../build.common.gradle' - -version = "1.0.0" - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_coroutines_version" - - implementation "io.github.spair:imgui-java-app:1.84.1.0" - - implementation 'com.google.code.gson:gson:2.8.7' - implementation 'io.github.classgraph:classgraph:4.8.108' -} diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt deleted file mode 100644 index f71247fb..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/EasyVision.kt +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (c) 2021 Sebastian Erives - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -package io.github.deltacv.easyvision - -import imgui.ImFont -import imgui.ImGui -import imgui.ImVec2 -import imgui.app.Application -import imgui.app.Configuration -import imgui.flag.* -import io.github.deltacv.easyvision.codegen.* -import io.github.deltacv.easyvision.gui.PopupBuilder -import io.github.deltacv.easyvision.gui.makeFont -import io.github.deltacv.easyvision.id.IdElementContainer -import io.github.deltacv.easyvision.node.NodeEditor -import io.github.deltacv.easyvision.node.NodeList -import io.github.deltacv.easyvision.node.vision.* -import org.lwjgl.BufferUtils -import org.lwjgl.glfw.GLFW -import org.lwjgl.glfw.GLFW.* -import org.lwjgl.glfw.GLFWKeyCallback - -class EasyVision : Application() { - - companion object { - private var ptr = 0L - - private val w = BufferUtils.createIntBuffer(1) - private val h = BufferUtils.createIntBuffer(1) - - val windowSize: ImVec2 get() { - w.position(0) - h.position(0) - - glfwGetWindowSize(ptr, w, h) - - return ImVec2(w.get(0).toFloat(), h.get(0).toFloat()) - } - - val miscIds = IdElementContainer() - } - - private var prevKeyCallback: GLFWKeyCallback? = null - - val nodeEditor = NodeEditor(this) - val nodeList = NodeList(this) - - val codeGenManager = CodeGenManager(this) - - private lateinit var defaultFont: ImFont - - fun start() { - nodeEditor.init() - - launch(this) - - nodeEditor.destroy() - } - - override fun configure(config: Configuration) { - config.title = "EasyVision" - } - - override fun initImGui(config: Configuration?) { - super.initImGui(config) - defaultFont = makeFont(13f) - nodeList.init() - } - - override fun process() { - if(prevKeyCallback == null) { - ptr = handle - // register a new key callback that will call the previous callback and handle some special keys - prevKeyCallback = glfwSetKeyCallback(handle, ::keyCallback) - } - - ImGui.setNextWindowPos(0f, 0f, ImGuiCond.Always) - - val size = windowSize - ImGui.setNextWindowSize(size.x, size.y, ImGuiCond.Always) - - ImGui.pushFont(defaultFont) - - ImGui.begin("Editor", - ImGuiWindowFlags.NoResize or ImGuiWindowFlags.NoMove - or ImGuiWindowFlags.NoCollapse or ImGuiWindowFlags.NoBringToFrontOnFocus - or ImGuiWindowFlags.NoTitleBar or ImGuiWindowFlags.NoDecoration - ) - - nodeEditor.draw() - - ImGui.end() - ImGui.popFont() - - nodeList.draw() - - ImGui.pushFont(defaultFont) - PopupBuilder.draw() - ImGui.popFont() - - isDeleteReleased = false - isEscReleased = false - isSpaceReleased = false - - if(ImGui.isMouseReleased(ImGuiMouseButton.Right)) { - codeGenManager.build() - } - } - - var isDeleteReleased = false - private set - var isEscReleased = false - private set - var isSpaceReleased = false - private set - - private fun keyCallback(windowId: Long, key: Int, scancode: Int, action: Int, mods: Int) { - if(prevKeyCallback != null) { - prevKeyCallback!!.invoke(windowId, key, scancode, action, mods) //invoke the imgui callback - } - - isDeleteReleased = scancode == 119 && action == GLFW.GLFW_RELEASE - isEscReleased = scancode == 9 && action == GLFW.GLFW_RELEASE - isSpaceReleased = scancode == 65 && action == GLFW.GLFW_RELEASE - } -} - -fun main() { - EasyVision().start() -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/Project.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/Project.kt deleted file mode 100644 index d4265f22..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/Project.kt +++ /dev/null @@ -1,4 +0,0 @@ -package io.github.deltacv.easyvision - -class Project { -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt deleted file mode 100644 index 4e5b58c7..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/Attribute.kt +++ /dev/null @@ -1,135 +0,0 @@ -package io.github.deltacv.easyvision.attribute - -import imgui.extension.imnodes.ImNodes -import io.github.deltacv.easyvision.codegen.CodeGen -import io.github.deltacv.easyvision.codegen.GenValue -import io.github.deltacv.easyvision.exception.AttributeGenException -import io.github.deltacv.easyvision.id.DrawableIdElement -import io.github.deltacv.easyvision.node.Link -import io.github.deltacv.easyvision.node.Node - -enum class AttributeMode { INPUT, OUTPUT } - -abstract class Attribute : DrawableIdElement { - - abstract val mode: AttributeMode - - override val id by lazy { parentNode.attributesIdContainer.nextId(this).value } - - lateinit var parentNode: Node<*> - internal set - - val links = mutableListOf() - val hasLink get() = links.isNotEmpty() - - val isInput by lazy { mode == AttributeMode.INPUT } - val isOutput by lazy { !isInput } - - private var isFirstDraw = true - - private var cancelNextDraw = false - var wasLastDrawCancelled = false - private set - - abstract fun drawAttribute() - - fun drawHere() { - draw() - cancelNextDraw = true - } - - override fun draw() { - if(cancelNextDraw) { - cancelNextDraw = false - wasLastDrawCancelled = true - return - } - - if(wasLastDrawCancelled) { - wasLastDrawCancelled = false - } - - if(isFirstDraw) { - enable() - isFirstDraw = false - } - - if(parentNode.drawAttributesCircles) { - if (mode == AttributeMode.INPUT) { - ImNodes.beginInputAttribute(id) - } else { - ImNodes.beginOutputAttribute(id) - } - } - - drawAttribute() - - if(parentNode.drawAttributesCircles) { - if (mode == AttributeMode.INPUT) { - ImNodes.endInputAttribute() - } else { - ImNodes.endOutputAttribute() - } - } - } - - override fun delete() { - Node.attributes.removeId(id) - - for(link in links.toTypedArray()) { - link.delete() - links.remove(link) - } - } - - override fun restore() { - Node.attributes[id] = this - } - - fun linkedAttribute(): Attribute? { - if(!isInput) { - raise("Output attributes might have more than one link, so linkedAttribute() is not allowed") - } - - if(!hasLink) { - return null - } - - val link = links[0] - - return if(link.aAttrib == this) { - link.bAttrib - } else link.aAttrib - } - - fun linkedAttributes() = links.map { - if(it.aAttrib == this) { - it.bAttrib - } else it.aAttrib - } - - fun raise(message: String): Nothing = throw AttributeGenException(this, message) - - fun warn(message: String) { - println("WARN: $message") // TODO: Warnings system... - } - - fun raiseAssert(condition: Boolean, message: String) { - if(!condition) { - raise(message) - } - } - - fun warnAssert(condition: Boolean, message: String) { - if(!condition) { - warn(message) - } - } - - abstract fun acceptLink(other: Attribute): Boolean - - abstract fun value(current: CodeGen.Current): GenValue - - protected fun getOutputValue(current: CodeGen.Current) = parentNode.getOutputValueOf(current, this) - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt deleted file mode 100644 index 59a5502c..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/TypedAttribute.kt +++ /dev/null @@ -1,44 +0,0 @@ -package io.github.deltacv.easyvision.attribute - -import imgui.ImGui -import io.github.deltacv.easyvision.codegen.CodeGen -import io.github.deltacv.easyvision.codegen.GenValue -import io.github.deltacv.easyvision.exception.AttributeGenException -import io.github.deltacv.easyvision.node.Link - -interface Type { - val name: String - val allowsNew: Boolean get() = true - - fun new(mode: AttributeMode, variableName: String): TypedAttribute { - throw UnsupportedOperationException("Cannot instantiate a List attribute with new") - } -} - -abstract class TypedAttribute(var type: Type) : Attribute() { - - abstract var variableName: String? - - var drawDescriptiveText = true - var drawType = true - - private val finalVarName by lazy { - variableName ?: if (mode == AttributeMode.INPUT) "Input" else "Output" - } - - override fun drawAttribute() { - if(drawDescriptiveText) { - ImGui.alignTextToFramePadding() - val t = if(drawType) { - "(${type.name}) " - } else "" - - ImGui.text("$t$finalVarName") - } else { - ImGui.text("") - } - } - - override fun acceptLink(other: Attribute) = this::class == other::class - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/BooleanAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/BooleanAttribute.kt deleted file mode 100644 index 9d93370c..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/BooleanAttribute.kt +++ /dev/null @@ -1,59 +0,0 @@ -package io.github.deltacv.easyvision.attribute.math - -import imgui.ImGui -import imgui.type.ImBoolean -import io.github.deltacv.easyvision.attribute.AttributeMode -import io.github.deltacv.easyvision.attribute.Type -import io.github.deltacv.easyvision.attribute.TypedAttribute -import io.github.deltacv.easyvision.codegen.CodeGen -import io.github.deltacv.easyvision.codegen.GenValue - -class BooleanAttribute( - override val mode: AttributeMode, - override var variableName: String? = null -) : TypedAttribute(Companion) { - - companion object: Type { - override val name = "Boolean" - - override fun new(mode: AttributeMode, variableName: String) = BooleanAttribute(mode, variableName) - } - - val value = ImBoolean() - - override fun drawAttribute() { - super.drawAttribute() - - if(!hasLink && mode == AttributeMode.INPUT) { - ImGui.checkbox("", value) - } - } - - override fun value(current: CodeGen.Current): GenValue.Boolean { - if(isInput) { - if(hasLink) { - val linkedAttrib = linkedAttribute() - - raiseAssert( - linkedAttrib != null, - "Boolean attribute must have another attribute attached" - ) - - val value = linkedAttrib!!.value(current) - raiseAssert(value is GenValue.Boolean, "Attribute attached is not a Boolean") - - return value as GenValue.Boolean - } else { - return if (value.get()) { - GenValue.Boolean.True - } else GenValue.Boolean.False - } - } else { - val value = getOutputValue(current) - raiseAssert(value is GenValue.Boolean, "Value returned from the node is not a Boolean") - - return value as GenValue.Boolean - } - } - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt deleted file mode 100644 index 3f73796f..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/math/IntegerAttribute.kt +++ /dev/null @@ -1,61 +0,0 @@ -package io.github.deltacv.easyvision.attribute.math - -import imgui.ImGui -import imgui.type.ImInt -import io.github.deltacv.easyvision.attribute.AttributeMode -import io.github.deltacv.easyvision.attribute.Type -import io.github.deltacv.easyvision.attribute.TypedAttribute -import io.github.deltacv.easyvision.codegen.CodeGen -import io.github.deltacv.easyvision.codegen.GenValue - -class IntAttribute( - override val mode: AttributeMode, - override var variableName: String? = null -) : TypedAttribute(Companion) { - - companion object: Type { - override val name = "Int" - - override fun new(mode: AttributeMode, variableName: String) = IntAttribute(mode, variableName) - } - - val value = ImInt() - - override fun drawAttribute() { - super.drawAttribute() - - if(!hasLink && mode == AttributeMode.INPUT) { - ImGui.sameLine() - - ImGui.pushItemWidth(110.0f) - ImGui.inputInt("", value) - ImGui.popItemWidth() - } - } - - override fun value(current: CodeGen.Current): GenValue.Int { - if(isInput) { - return if(hasLink) { - val linkedAttrib = linkedAttribute() - - raiseAssert( - linkedAttrib != null, - "Int attribute must have another attribute attached" - ) - - val value = linkedAttrib!!.value(current) - raiseAssert(value is GenValue.Int, "Attribute attached is not an Int") - - value as GenValue.Int - } else { - GenValue.Int(value.get()) - } - } else { - val value = getOutputValue(current) - raiseAssert(value is GenValue.Int, "Value returned from the node is not an Int") - - return value as GenValue.Int - } - } - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/EnumAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/EnumAttribute.kt deleted file mode 100644 index 394c3eed..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/EnumAttribute.kt +++ /dev/null @@ -1,84 +0,0 @@ -package io.github.deltacv.easyvision.attribute.misc - -import imgui.ImGui -import imgui.type.ImInt -import io.github.deltacv.easyvision.attribute.Attribute -import io.github.deltacv.easyvision.attribute.AttributeMode -import io.github.deltacv.easyvision.attribute.Type -import io.github.deltacv.easyvision.attribute.TypedAttribute -import io.github.deltacv.easyvision.codegen.CodeGen -import io.github.deltacv.easyvision.codegen.GenValue - -class EnumAttribute>( - override val mode: AttributeMode, - val values: Array, - override var variableName: String? -) : TypedAttribute(Companion) { - - companion object: Type { - override val name = "Enum" - override val allowsNew = false - } - - private val valuesStrings = values.map { - it.name - }.toTypedArray() - - val currentItem = ImInt() - - override fun drawAttribute() { - super.drawAttribute() - - if(!hasLink) { - ImGui.pushItemWidth(110.0f) - ImGui.combo("", currentItem, valuesStrings) - ImGui.popItemWidth() - } - } - - override fun acceptLink(other: Attribute) = other is EnumAttribute<*> && values[0]::class == other.values[0]::class - - @Suppress("UNCHECKED_CAST") - override fun value(current: CodeGen.Current): GenValue.Enum { - val expectedClass = values[0]::class - - if(isInput) { - if(hasLink) { - val linkedAttrib = linkedAttribute() - - raiseAssert( - linkedAttrib != null, - "Enum attribute must have another attribute attached" - ) - - val value = linkedAttrib!!.value(current) - raiseAssert(value is GenValue.Enum<*>, "Attribute attached is not a valid Enum") - - val valueEnum = value as GenValue.Enum<*> - - raiseAssert( - value.clazz == expectedClass, - "Enum attribute attached (${value.clazz}) is not the expected type of enum ($expectedClass)" - ) - - return valueEnum as GenValue.Enum - } else { - val value = values[currentItem.get()] - - return GenValue.Enum(value, value::class.java) - } - } else { - val value = getOutputValue(current) - raiseAssert(value is GenValue.Enum<*>, "Value returned from the node is not an enum") - - val valueEnum = value as GenValue.Enum - raiseAssert( - value.clazz == expectedClass, - "Enum attribute returned from the node (${value.clazz}) is not the expected type of enum ($expectedClass)" - ) - - return valueEnum - } - } - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt deleted file mode 100644 index 7e40efb0..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/misc/ListAttribute.kt +++ /dev/null @@ -1,155 +0,0 @@ -package io.github.deltacv.easyvision.attribute.misc - -import imgui.ImGui -import io.github.deltacv.easyvision.attribute.Attribute -import io.github.deltacv.easyvision.attribute.AttributeMode -import io.github.deltacv.easyvision.attribute.Type -import io.github.deltacv.easyvision.attribute.TypedAttribute -import io.github.deltacv.easyvision.codegen.CodeGen -import io.github.deltacv.easyvision.codegen.GenValue - -open class ListAttribute( - override val mode: AttributeMode, - val elementType: Type, - override var variableName: String? = null, - length: Int? = null, - private val allowAddOrDelete: Boolean = true -) : TypedAttribute(Companion) { - - companion object: Type { - override val name = "List" - override val allowsNew = false - } - - val listAttributes = mutableListOf() - val deleteQueue = mutableListOf() - - private var beforeHasLink = false - - private var previousLength: Int? = 0 - var fixedLength = length - set(value) { - field = value - onEnable() - } - - private val allowAod get() = allowAddOrDelete && fixedLength == null - - override fun onEnable() { - // oh god... (it's been only 10 minutes and i have already forgotten how this works) - if(previousLength != fixedLength) { - if(fixedLength != null && (previousLength == null || previousLength == 0)) { - repeat(fixedLength!!) { - createElement() - } - } else if(previousLength != null || previousLength != 0) { - val delta = (fixedLength ?: 0) - (previousLength ?: 0) - - if(delta < 0) { - repeat(-delta) { - val last = listAttributes[listAttributes.size - 1] - last.delete() - - listAttributes.remove(last) - deleteQueue.add(last) - } - } else { - repeat(delta) { - if(deleteQueue.isNotEmpty()) { - val last = deleteQueue[deleteQueue.size - 1] - last.restore() - - listAttributes.add(last) - deleteQueue.remove(last) - } else { - createElement() - } - } - } - } else { - for(attribute in listAttributes.toTypedArray()) { - attribute.delete() - } - } - } - - previousLength = fixedLength - } - - override fun draw() { - super.draw() - - for((i, attrib) in listAttributes.withIndex()) { - if(beforeHasLink != hasLink) { - if(hasLink) { - // delete attributes if a link has been created - attrib.delete() - } else { - // restore list attribs if they were previously deleted - // after destroying a link with another node - attrib.restore() - } - } - - if(!hasLink) { // only draw attributes if there's not a link attached - drawAttributeText(i) - attrib.draw() - } - } - - beforeHasLink = hasLink - } - - open fun drawAttributeText(index: Int) { } - - override fun value(current: CodeGen.Current): GenValue = - // get the values of all the attributes and return a - // GenValue.List with the attribute values in an array - GenValue.List(listAttributes.map { it.value(current) }.toTypedArray()) - - override fun drawAttribute() { - ImGui.text("[${elementType.name}] $variableName") - - if(!hasLink && elementType.allowsNew && allowAod && mode == AttributeMode.INPUT) { - // idk wat the frame height is, i just stole it from - // https://github.com/ocornut/imgui/blob/7b8bc864e9af6c6c9a22125d65595d526ba674c5/imgui_widgets.cpp#L3439 - - val buttonSize = ImGui.getFrameHeight() - - val style = ImGui.getStyle() - - ImGui.sameLine(0.0f, style.itemInnerSpacingX * 2.0f) - - if(ImGui.button("+", buttonSize, buttonSize)) { // creates a new element with the + button - // uses the "new" function from the attribute's companion Type - createElement() - } - - // display the - button only if the attributes list is not empty - if(listAttributes.isNotEmpty()) { - ImGui.sameLine(0.0f, style.itemInnerSpacingX) - - if(ImGui.button("-", buttonSize, buttonSize)) { - // remove the last element from the list when - is pressed - listAttributes.removeLastOrNull() - ?.delete() // also delete it from the element id registry - } - } - } - } - - override fun acceptLink(other: Attribute) = other is ListAttribute && other.elementType == elementType - - private fun createElement() { - val count = listAttributes.size.toString() - val elementName = count + if(count.length == 1) " " else "" - - val element = elementType.new(AttributeMode.INPUT, elementName) - element.parentNode = parentNode - element.enable() //enables the new element - - element.drawType = false // hides the variable type - - listAttributes.add(element) - } -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/MatAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/MatAttribute.kt deleted file mode 100644 index 160adba6..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/MatAttribute.kt +++ /dev/null @@ -1,41 +0,0 @@ -package io.github.deltacv.easyvision.attribute.vision - -import io.github.deltacv.easyvision.attribute.TypedAttribute -import io.github.deltacv.easyvision.attribute.AttributeMode -import io.github.deltacv.easyvision.attribute.Type -import io.github.deltacv.easyvision.codegen.CodeGen -import io.github.deltacv.easyvision.codegen.GenValue - -class MatAttribute( - override val mode: AttributeMode, - override var variableName: String? = null -) : TypedAttribute(Companion) { - - companion object: Type { - override val name = "Image" - - override fun new(mode: AttributeMode, variableName: String) = MatAttribute(mode, variableName) - } - - override fun value(current: CodeGen.Current): GenValue.Mat { - if(isInput) { - val linkedAttrib = linkedAttribute() - - raiseAssert( - linkedAttrib != null, - "Mat attribute must have another attribute attached" - ) - - val value = linkedAttrib!!.value(current) - raiseAssert(value is GenValue.Mat, "Attribute attached is not a Mat") - - return value as GenValue.Mat - } else { - val value = getOutputValue(current) - raiseAssert(value is GenValue.Mat, "Value returned from the node is not a Mat") - - return value as GenValue.Mat - } - } - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/RangeAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/RangeAttribute.kt deleted file mode 100644 index f8eb02ae..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/RangeAttribute.kt +++ /dev/null @@ -1,82 +0,0 @@ -package io.github.deltacv.easyvision.attribute.vision - -import imgui.type.ImInt -import io.github.deltacv.easyvision.EasyVision -import io.github.deltacv.easyvision.attribute.AttributeMode -import io.github.deltacv.easyvision.attribute.Type -import io.github.deltacv.easyvision.attribute.TypedAttribute -import io.github.deltacv.easyvision.codegen.CodeGen -import io.github.deltacv.easyvision.codegen.GenValue -import io.github.deltacv.easyvision.gui.ExtraWidgets - -class RangeAttribute( - override val mode: AttributeMode, - override var variableName: String? = null -) : TypedAttribute(Companion) { - - companion object : Type { - override val name = "Range" - - override fun new(mode: AttributeMode, variableName: String) = RangeAttribute(mode, variableName) - } - - var min = 0 - var max = 255 - - val minValue = ImInt(min) - val maxValue = ImInt(max) - - private val minId by EasyVision.miscIds.nextId() - private val maxId by EasyVision.miscIds.nextId() - - override fun drawAttribute() { - if(!hasLink) { - ExtraWidgets.rangeSliders( - min, max, - minValue, maxValue, - minId, maxId, - width = 95f - ) - - val mn = minValue.get() - val mx = maxValue.get() - - if(mn > mx) { - minValue.set(mx) - } - if(mx < mn) { - maxValue.set(mn) - } - } - } - - override fun value(current: CodeGen.Current): GenValue.Range { - if(isInput) { - return if(hasLink) { - val linkedAttrib = linkedAttribute() - - raiseAssert( - linkedAttrib != null, - "Range attribute must have another attribute attached" - ) - - val value = linkedAttrib!!.value(current) - raiseAssert(value is GenValue.Range, "Attribute attached is not a Range") - - value as GenValue.Range - } else { - GenValue.Range( - minValue.get().toDouble(), - maxValue.get().toDouble() - ) - } - } else { - val value = getOutputValue(current) - raiseAssert(value is GenValue.Range, "Value returned from the node is not a Range") - - return value as GenValue.Range - } - } - - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/ScalarAttribute.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/ScalarAttribute.kt deleted file mode 100644 index 48c52e4c..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/attribute/vision/ScalarAttribute.kt +++ /dev/null @@ -1,50 +0,0 @@ -package io.github.deltacv.easyvision.attribute.vision - -import imgui.ImGui -import io.github.deltacv.easyvision.attribute.AttributeMode -import io.github.deltacv.easyvision.attribute.misc.ListAttribute -import io.github.deltacv.easyvision.codegen.CodeGen -import io.github.deltacv.easyvision.codegen.GenValue -import io.github.deltacv.easyvision.node.vision.Colors - -class ScalarAttribute( - mode: AttributeMode, - color: Colors, - variableName: String? = null -) : ListAttribute(mode, RangeAttribute, variableName, color.channels) { - - var color = color - set(value) { - fixedLength = value.channels - field = value - } - - override fun drawAttributeText(index: Int) { - if(index < color.channelNames.size) { - val name = color.channelNames[index] - val elementName = name + if(name.length == 1) " " else "" - - ImGui.text(elementName) - ImGui.sameLine() - } - } - - override fun value(current: CodeGen.Current): GenValue.ScalarRange { - val values = (super.value(current) as GenValue.List).elements - val ZERO = GenValue.Range.ZERO - - return GenValue.ScalarRange( - values.getOr(0, ZERO) as GenValue.Range, - values.getOr(1, ZERO) as GenValue.Range, - values.getOr(2, ZERO) as GenValue.Range, - values.getOr(3, ZERO) as GenValue.Range - ) - } - -} - -fun Array.getOr(index: Int, or: T) = try { - this[index] -} catch(ignored: ArrayIndexOutOfBoundsException) { - or -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt deleted file mode 100644 index df7b3631..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGen.kt +++ /dev/null @@ -1,95 +0,0 @@ -package io.github.deltacv.easyvision.codegen - -import io.github.deltacv.easyvision.codegen.parse.Parameter -import io.github.deltacv.easyvision.codegen.parse.Scope -import io.github.deltacv.easyvision.codegen.dsl.CodeGenContext -import io.github.deltacv.easyvision.node.Node - -enum class Visibility { - PUBLIC, PRIVATE, PROTECTED -} - -class CodeGen(var className: String) { - - val importScope = Scope(0) - val classStartScope = Scope(1) - val classEndScope = Scope(1) - - val initScope = Scope(2) - val currScopeInit = Current(this, initScope) - - val processFrameScope = Scope(2) - val currScopeProcessFrame = Current(this, processFrameScope) - - val viewportTappedScope = Scope(2) - val currScopeViewportTapped = Current(this, viewportTappedScope) - - val sessions = mutableMapOf, CodeGenSession>() - - init { - importScope.run { - import("org.openftc.easyopencv.OpenCvPipeline") - import("org.opencv.core.Mat") - } - } - - fun gen(): String { - val mainScope = Scope(0) - val bodyScope = Scope(1) - - val start = classStartScope.get() - if(start.isNotBlank()) { - bodyScope.scope(classStartScope) - bodyScope.newStatement() - } - - val init = initScope.get() - if(init.isNotBlank()) { - bodyScope.method( - Visibility.PUBLIC, "void", "init", initScope, - Parameter("Mat", "input"), isOverride = true - ) - bodyScope.newStatement() - } - - val process = processFrameScope.get() - bodyScope.method( - Visibility.PUBLIC, "Mat", "processFrame", processFrameScope, - Parameter("Mat", "input"), isOverride = true - ) - - val viewportTapped = viewportTappedScope.get() - if(viewportTapped.isNotBlank()) { - bodyScope.newStatement() - - bodyScope.method( - Visibility.PUBLIC, "Mat", "onViewportTapped", viewportTappedScope, - isOverride = true - ) - } - - val end = classEndScope.get() - if(end.isNotBlank()) { - bodyScope.scope(classEndScope) - } - - mainScope.scope(importScope) - mainScope.newStatement() - mainScope.clazz(Visibility.PUBLIC, className, bodyScope, extends = arrayOf("OpenCvPipeline")) - - return mainScope.get() - } - - private val context = CodeGenContext(this) - - operator fun invoke(block: CodeGenContext.() -> T) = block(context) - - data class Current(val codeGen: CodeGen, val scope: Scope) { - operator fun invoke(scopeBlock: CodeGenContext.() -> T) = codeGen.invoke(scopeBlock) - } - -} - -interface CodeGenSession - -object NoSession : CodeGenSession \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGenManager.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGenManager.kt deleted file mode 100644 index 07ddb262..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/CodeGenManager.kt +++ /dev/null @@ -1,18 +0,0 @@ -package io.github.deltacv.easyvision.codegen - -import io.github.deltacv.easyvision.EasyVision -import io.github.deltacv.easyvision.util.ElapsedTime - -class CodeGenManager(val easyVision: EasyVision) { - - fun build() { - val timer = ElapsedTime() - - val codeGen = CodeGen("TestPipeline") - easyVision.nodeEditor.inputNode.startGen(codeGen.currScopeProcessFrame) - - println(codeGen.gen()) - println("took ${timer.seconds}") - } - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Csv.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Csv.kt deleted file mode 100644 index 7a8b5703..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/Csv.kt +++ /dev/null @@ -1,28 +0,0 @@ -package io.github.deltacv.easyvision.codegen - -import io.github.deltacv.easyvision.codegen.parse.Parameter -import io.github.deltacv.easyvision.codegen.parse.Value - -fun Array.csv(): String { - val builder = StringBuilder() - - for((i, parameter) in this.withIndex()) { - builder.append(parameter) - - if(i < this.size - 1) { - builder.append(", ") - } - } - - return builder.toString() -} - -fun Array.csv(): String { - val stringArray = this.map { "${it.type} ${it.name}" }.toTypedArray() - return stringArray.csv() -} - -fun Array.csv(): String { - val stringArray = this.map { it.value!! }.toTypedArray() - return stringArray.csv() -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/GenValue.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/GenValue.kt deleted file mode 100644 index 6ca1f7bb..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/GenValue.kt +++ /dev/null @@ -1,52 +0,0 @@ -package io.github.deltacv.easyvision.codegen - -import io.github.deltacv.easyvision.attribute.Attribute -import io.github.deltacv.easyvision.codegen.parse.Value -import io.github.deltacv.easyvision.node.vision.Colors - -sealed class GenValue { - - data class Mat(val value: Value, val color: Colors, val isBinary: kotlin.Boolean = false) : GenValue() { - fun requireBinary(attribute: Attribute) { - attribute.warnAssert( - isBinary, - "Input Mat is not binary as required, this might cause runtime issues." - ) - } - - fun requireNonBinary(attribute: Attribute) { - attribute.warnAssert( - !isBinary, - "Input Mat is binary where it shouldn't be, this might cause runtime issues." - ) - } - } - - data class Enum>(val value: E, val clazz: Class<*>) : GenValue() - - data class Int(val value: kotlin.Int) : GenValue() - data class Float(val value: kotlin.Float) : GenValue() - data class Double(val value: kotlin.Double) : GenValue() - - data class Range(val min: kotlin.Double, val max: kotlin.Double) : GenValue(){ - companion object { - val ZERO = Range(0.0, 0.0) - } - } - - data class ScalarRange(val a: Range, val b: Range, val c: Range, val d: Range) : GenValue() { - companion object { - val ZERO = ScalarRange(Range.ZERO, Range.ZERO, Range.ZERO, Range.ZERO) - } - } - - sealed class Boolean(val value: kotlin.Boolean) : GenValue() { - object True : Boolean(true) - object False : Boolean(false) - } - - data class List(val elements: Array) : GenValue() - - object None : GenValue() - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/CodeGenContext.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/CodeGenContext.kt deleted file mode 100644 index e92e58ea..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/CodeGenContext.kt +++ /dev/null @@ -1,54 +0,0 @@ -package io.github.deltacv.easyvision.codegen.dsl - -import io.github.deltacv.easyvision.codegen.* -import io.github.deltacv.easyvision.codegen.parse.* - -class CodeGenContext(val codeGen: CodeGen) { - - fun import(pkg: String) { - codeGen.importScope.import(pkg) - } - - fun enum(name: String, vararg values: String) { - codeGen.classStartScope.enumClass(name, *values) - } - - fun init(block: ScopeContext.() -> Unit) { - codeGen.initScope(block) - } - - fun processFrame(block: ScopeContext.() -> Unit) { - codeGen.processFrameScope(block) - } - - fun onViewportTapped(block: ScopeContext.() -> Unit) { - codeGen.viewportTappedScope(block) - } - - fun public(name: String, v: Value) = - codeGen.classStartScope.instanceVariable(Visibility.PUBLIC, name, v) - - fun private(name: String, v: Value) = - codeGen.classStartScope.instanceVariable(Visibility.PRIVATE, name, v) - - fun protected(name: String, v: Value) = - codeGen.classStartScope.instanceVariable(Visibility.PROTECTED, name, v) - - fun tryName(name: String) = codeGen.classStartScope.tryName(name) - - operator fun String.invoke( - vis: Visibility, returnType: String, - vararg parameters: Parameter, - isStatic: Boolean = false, isFinal: Boolean = false, isOverride: Boolean = true, - scopeBlock: ScopeContext.() -> Unit - ) { - val s = Scope(2) - scopeBlock(s.context) - - codeGen.classEndScope.method( - vis, returnType, this, s, *parameters, - isStatic = isStatic, isFinal = isFinal, isOverride = isOverride - ) - } - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/ScopeContext.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/ScopeContext.kt deleted file mode 100644 index b002efd5..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/dsl/ScopeContext.kt +++ /dev/null @@ -1,31 +0,0 @@ -package io.github.deltacv.easyvision.codegen.dsl - -import io.github.deltacv.easyvision.codegen.parse.Scope -import io.github.deltacv.easyvision.codegen.parse.Value -import io.github.deltacv.easyvision.codegen.Visibility - -class ScopeContext(val scope: Scope) { - - var appendWhiteline: Boolean - get() = scope.appendWhiteline - set(value) { scope.appendWhiteline = value } - - operator fun String.invoke(vararg parameters: Value) { - scope.methodCall(this, *parameters) - } - - infix fun String.value(v: Value) = - scope.instanceVariable(Visibility.PUBLIC, this, v) - - fun String.local(name: String, v: Value) = - scope.localVariable(name, v) - - infix fun String.set(v: Value) = - scope.variableSet(this, v) - - infix fun String.instanceSet(v: Value) = - scope.instanceVariableSet(this, v) - - fun returnMethod(value: Value? = null) = scope.returnMethod(value) - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/parse/Scope.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/parse/Scope.kt deleted file mode 100644 index aa4f9606..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/parse/Scope.kt +++ /dev/null @@ -1,199 +0,0 @@ -package io.github.deltacv.easyvision.codegen.parse - -import io.github.deltacv.easyvision.codegen.dsl.ScopeContext -import io.github.deltacv.easyvision.codegen.* - -class Scope(private val tabsCount: Int = 1) { - - private var builder = StringBuilder() - - private val usedNames = mutableListOf() - - private val tabs by lazy { - val builder = StringBuilder() - - repeat(tabsCount) { - builder.append("\t") - } - - builder.toString() - } - - private val imports = mutableListOf() - - fun import(pkg: String) { - if(!imports.contains(pkg)) { - newStatement() - - imports.add(pkg) - - builder.append("import $pkg;") - } - } - - fun instanceVariable(vis: Visibility, name: String, - variable: Value, - isStatic: Boolean = false, isFinal: Boolean = false) { - newStatement() - usedNames.add(name) - - val modifiers = if(isStatic) " static" else "" + - if(isFinal) " final" else "" - - val ending = if(variable.value != null) "= ${variable.value};" else ";" - - builder.append("$tabs${vis.name.lowercase()}$modifiers ${variable.type} $name $ending") - } - - fun localVariable(name: String, variable: Value) { - newStatement() - usedNames.add(name) - - val ending = if(variable.value != null) "= ${variable.value};" else ";" - - builder.append("$tabs${variable.type} $name $ending") - } - - fun tryName(name: String): String { - if(!usedNames.contains(name)) { - return name - } else { - var count = 1 - - while(true) { - val newName = "$name$count" - - if(!usedNames.contains(newName)) { - return newName - } - - count++ - } - } - } - - fun variableSet(name: String, v: Value) { - newStatement() - - builder.append("$tabs$name = ${v.value!!};") - } - - fun instanceVariableSet(name: String, v: Value) { - newStatement() - - builder.append("${tabs}this.$name = ${v.value!!};") - } - - fun methodCall(className: String, methodName: String, vararg parameters: Value) { - newStatement() - - builder.append("$tabs$className.$methodName(${parameters.csv()});") - } - - fun methodCall(methodName: String, vararg parameters: Value) { - newStatement() - - builder.append("$tabs$methodName(${parameters.csv()});") - } - - fun method( - vis: Visibility, returnType: String, name: String, body: Scope, - vararg parameters: Parameter, - isStatic: Boolean = false, isFinal: Boolean = false, isOverride: Boolean = false - ) { - newLineIfNotBlank() - - val static = if(isStatic) "static " else "" - val final = if(isFinal) "final " else "" - - if(isOverride) { - builder.append("$tabs@Override").appendLine() - } - - builder.append(""" - |$tabs${vis.name.lowercase()} $static$final$returnType $name(${parameters.csv()}) { - |$body - |$tabs} - """.trimMargin()) - } - - fun returnMethod(value: Value? = null) { - newStatement() - - if(value != null) { - builder.append("${tabs}return ${value.value!!};") - } else { - builder.append("${tabs}return;") - } - } - - fun clazz(vis: Visibility, name: String, body: Scope, - extends: Array = arrayOf(), implements: Array = arrayOf(), - isStatic: Boolean = false, isFinal: Boolean = false) { - - newStatement() - - val static = if(isStatic) "static " else "" - val final = if(isFinal) "final " else "" - - val e = if(extends.isNotEmpty()) "extends ${extends.csv()} " else "" - val i = if(implements.isNotEmpty()) "implements ${implements.csv()} " else "" - - val endWhitespaceLine = if(!body.get().endsWith("\n")) "\n" else "" - - builder.append(""" - |$tabs${vis.name.lowercase()} $static${final}class $name $e$i{ - |$body$endWhitespaceLine - |$tabs} - """.trimMargin()) - } - - fun enumClass(name: String, vararg values: String) { - newStatement() - - builder.append("${tabs}enum $name { ${values.csv()} }") - } - - fun scope(scope: Scope) { - newLineIfNotBlank() - builder.append(scope) - } - - fun newStatement() { - if(builder.isNotEmpty()) { - builder.appendLine() - } - } - - fun newLineIfNotBlank() { - val str = get() - - if(!str.endsWith("\n\n") && str.endsWith("\n")) { - builder.appendLine() - } else if(!str.endsWith("\n\n")) { - builder.append("\n") - } - } - - fun clear() = builder.clear() - - fun get() = builder.toString() - - override fun toString() = get() - - internal val context = ScopeContext(this) - - var appendWhiteline = true - - operator fun invoke(block: ScopeContext.() -> Unit) { - block(context) - - if(appendWhiteline) { - newStatement() - } - appendWhiteline = true - } - -} - -data class Parameter(val type: String, val name: String) \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/parse/Value.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/parse/Value.kt deleted file mode 100644 index 3f5433d6..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/parse/Value.kt +++ /dev/null @@ -1,23 +0,0 @@ -package io.github.deltacv.easyvision.codegen.parse - -import io.github.deltacv.easyvision.codegen.csv -import io.github.deltacv.easyvision.node.vision.Colors - -fun new(type: String, vararg parameters: String) = Value(type, "new $type(${parameters.csv()})") - -fun value(type: String, value: String) = Value(type, value) - -fun callValue(methodName: String, returnType: String, vararg parameters: Value) = - Value(returnType, "$methodName(${parameters.csv()})") - -fun enumValue(type: String, constantName: String) = Value(type, "$type.$constantName") - -fun cvtColorValue(a: Colors, b: Colors) = Value("int", "Imgproc.COLOR_${a.name}2${b.name}") - -fun variable(type: String) = Value(type, null) - -val String.v get() = Value("", this) - -val Number.v get() = toString().v - -data class Value(val type: String, val value: String?) \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/vision/MatRecycle.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/vision/MatRecycle.kt deleted file mode 100644 index 3e22642b..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/codegen/vision/MatRecycle.kt +++ /dev/null @@ -1,4 +0,0 @@ -package io.github.deltacv.easyvision.codegen.vision - -class MatRecycle { -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/exception/GenException.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/exception/GenException.kt deleted file mode 100644 index a6894984..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/exception/GenException.kt +++ /dev/null @@ -1,8 +0,0 @@ -package io.github.deltacv.easyvision.exception - -import io.github.deltacv.easyvision.attribute.Attribute -import io.github.deltacv.easyvision.node.Node - -class NodeGenException(val node: Node<*>, override val message: String) : RuntimeException(message) - -class AttributeGenException(val attribute: Attribute, override val message: String) : RuntimeException(message) \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/gui/ExtraWidgets.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/gui/ExtraWidgets.kt deleted file mode 100644 index a490b4ee..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/gui/ExtraWidgets.kt +++ /dev/null @@ -1,86 +0,0 @@ -package io.github.deltacv.easyvision.gui - -import imgui.ImFont -import imgui.ImFontConfig -import imgui.ImGui -import imgui.internal.ImRect -import imgui.internal.flag.ImGuiAxis -import imgui.type.ImFloat -import imgui.type.ImInt -import java.lang.Math.random - -object ExtraWidgets { - - fun rangeSliders(min: Int, max: Int, - minValue: ImInt, maxValue: ImInt, - minId: Int, maxId: Int, - width: Float = 110f) { - ImGui.pushItemWidth(width) - ImGui.sliderInt("###$minId", minValue.data, min, max) - - ImGui.sameLine() - - ImGui.sliderInt("###$maxId", maxValue.data, min, max) - ImGui.popItemWidth() - } - - private val valuesStringCache = mutableMapOf, Array>() - - fun > enumCombo(values: Array, currentItem: ImInt): T { - val clazz = values[0]::class.java - - val valuesStrings = if (valuesStringCache.containsKey(clazz)) { - valuesStringCache[clazz]!! - } else { - val v = values.map { - it.name - }.toTypedArray() - valuesStringCache[clazz] = v - - v - } - - ImGui.combo("", currentItem, valuesStrings) - - return values[currentItem.get()] - } - - fun splitter(id: Int, splitVertically: Boolean, thickness: Float, - size1: ImFloat, size2: ImFloat, - minSize1: Float, minSize2: Float, - splitterLongAxisSize: Float = -1.0f): Boolean { - - val cursorPos = ImGui.getCursorPos() - - val minX = cursorPos.x + if(splitVertically) size1.get() else 0f - val minY = cursorPos.y + if(splitVertically) 0f else size1.get() - - val maxX = minX + imgui.internal.ImGui.calcItemSizeX( - if(splitVertically) thickness else splitterLongAxisSize, - if(splitVertically) splitterLongAxisSize else thickness, - 0f, 0f - ) - val maxY = minY + imgui.internal.ImGui.calcItemSizeY( - if(splitVertically) splitterLongAxisSize else thickness, - if(splitVertically) thickness else splitterLongAxisSize, - 0f, 0f - ) - - return imgui.internal.ImGui.splitterBehavior( - minX, minY, maxX, maxY, id, - if(splitVertically) ImGuiAxis.X else ImGuiAxis.Y, - size1, size2, minSize1, minSize2 - ) - } - -} - -fun makeFont(size: Float): ImFont { - val fontConfig = ImFontConfig() - fontConfig.sizePixels = size - fontConfig.oversampleH = 1 - fontConfig.oversampleV = 1 - fontConfig.pixelSnapH = false - - return ImGui.getIO().fonts.addFontDefault(fontConfig) -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/gui/PopupBuilder.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/gui/PopupBuilder.kt deleted file mode 100644 index 84f3ea9c..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/gui/PopupBuilder.kt +++ /dev/null @@ -1,93 +0,0 @@ -package io.github.deltacv.easyvision.gui - -import imgui.ImGui -import io.github.deltacv.easyvision.EasyVision -import io.github.deltacv.easyvision.util.ElapsedTime - -object PopupBuilder { - - private val tooltips = mutableListOf() - - private val labels = mutableMapOf() - - fun addWarningToolTip(message: String, w: Float? = null, h: Float? = null) { - deleteLabel("WARN") - - val windowSize = EasyVision.windowSize - - var x = windowSize.x * 0.5f - var y = windowSize.y * 0.85f - - val wW = w ?: message.length * 7.5f - val wH = h ?: 30f - - x -= wW / 2f - y += wH / 2f - - addToolTip(x, y, wW, wH, message, 6.0, label = "WARN") - } - - fun addToolTip(x: Float, y: Float, w: Float? = null, h: Float? = null, - message: String, time: Double? = null, label: String = "") { - addToolTip(x, y, w, h, time, label) { - ImGui.text(message) - } - } - - - fun addToolTip(x: Float, y: Float, w: Float? = null, h: Float? = null, - time: Double? = null, label: String = "", drawCallback: () -> Unit) { - val tooltip = ToolTip(x, y, w, h, time, drawCallback) - tooltips.add(tooltip) - - labels[label] = Label(tooltip) { - tooltips.remove(tooltip) - } - } - - fun deleteLabel(label: String) { - labels[label]?.deleteCall?.invoke() - labels.remove(label) - } - - fun draw() { - for(tooltip in tooltips.toTypedArray()) { - if(tooltip.time != null && tooltip.elapsedTime.seconds >= tooltip.time) { - tooltips.remove(tooltip) - - for(label in labels.values.toTypedArray()) { - if(label.any == tooltip) { - label.deleteCall() - } - } - - continue - } - - tooltip.draw() - } - } - - private data class ToolTip(val x: Float, val y: Float, val w: Float?, val h: Float?, - val time: Double?, val callback: () -> Unit) { - - val elapsedTime by lazy { ElapsedTime() } - - fun draw() { - elapsedTime.seconds - - ImGui.setNextWindowPos(x, y) - if(w != null && h != null) { - ImGui.setNextWindowSize(w, h) - } - - ImGui.beginTooltip() - callback() - ImGui.endTooltip() - } - - } - - private data class Label(val any: Any, val deleteCall: () -> Unit) - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/id/IdElement.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/id/IdElement.kt deleted file mode 100644 index a2a4aef1..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/id/IdElement.kt +++ /dev/null @@ -1,23 +0,0 @@ -package io.github.deltacv.easyvision.id - -interface IdElement { - val id: Int -} - -interface DrawableIdElement : IdElement { - - fun draw() - - fun delete() - - fun restore() - - fun onEnable() { } - - fun enable(): DrawableIdElement { - ::id.get() - onEnable() - return this - } - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/id/IdElementContainer.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/id/IdElementContainer.kt deleted file mode 100644 index 1aa17a09..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/id/IdElementContainer.kt +++ /dev/null @@ -1,44 +0,0 @@ -package io.github.deltacv.easyvision.id - -class IdElementContainer : Iterable { - - private val e = ArrayList() - - /** - * Note that the element positions in this list won't necessarily match their ids - */ - var elements = ArrayList() - private set - - fun nextId(element: () -> T) = lazy { - nextId(element()).value - } - - fun nextId(element: T) = lazy { - e.add(element) - elements.add(element) - - e.lastIndexOf(element) - } - - fun nextId() = lazy { - e.add(null) - e.lastIndexOf(null) - } - - fun removeId(id: Int) { - elements.remove(e[id]) - e[id] = null - } - - operator fun get(id: Int) = e[id] - - operator fun set(id: Int, element: T) { - e[id] = element - - if(!elements.contains(element)) - elements.add(element) - } - - override fun iterator() = elements.listIterator() -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/DrawNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/DrawNode.kt deleted file mode 100644 index b4090334..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/DrawNode.kt +++ /dev/null @@ -1,75 +0,0 @@ -package io.github.deltacv.easyvision.node - -import imgui.ImGui -import imgui.ImVec2 -import imgui.extension.imnodes.ImNodes -import imgui.flag.ImGuiMouseButton -import io.github.deltacv.easyvision.codegen.CodeGenSession - -abstract class DrawNode( - var title: String? = null, - allowDelete: Boolean = true -) : Node(allowDelete) { - - var nextNodePosition: ImVec2? = null - - var pinToMouse = false - private var lastPinToMouse = false - - private var pinToMouseOffset = ImVec2() - - var isFirstDraw = true - - open fun init() {} - - override fun draw() { - ImNodes.beginNode(id) - if(title != null) { - ImNodes.beginNodeTitleBar() - ImGui.textUnformatted(title!!) - ImNodes.endNodeTitleBar() - } - - drawNode() - drawAttributes() - ImNodes.endNode() - - if(isFirstDraw) { - init() - isFirstDraw = false - } - - nextNodePosition?.let { - ImNodes.setNodeScreenSpacePos(id, it.x, it.y) - nextNodePosition = null - } - - if(pinToMouse) { - val mousePos = ImGui.getMousePos() - - if(pinToMouse != lastPinToMouse) { - val nodeDims = ImVec2() - ImNodes.getNodeDimensions(id, nodeDims) - - pinToMouseOffset = ImVec2( - nodeDims.x / 2, - nodeDims.y / 2 - ) - } - - val newPosX = mousePos.x - pinToMouseOffset.x - val newPosY = mousePos.y - pinToMouseOffset.y - - ImNodes.setNodeEditorSpacePos(id, newPosX, newPosY) - - if(ImGui.isMouseReleased(ImGuiMouseButton.Left)) { - pinToMouse = false - } - } - - lastPinToMouse = pinToMouse - } - - open fun drawNode() { } - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Link.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Link.kt deleted file mode 100644 index f52f66eb..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Link.kt +++ /dev/null @@ -1,60 +0,0 @@ -package io.github.deltacv.easyvision.node - -import imgui.extension.imnodes.ImNodes -import io.github.deltacv.easyvision.attribute.Attribute -import io.github.deltacv.easyvision.id.DrawableIdElement -import io.github.deltacv.easyvision.id.IdElementContainer - -class Link(val a: Int, val b: Int) : DrawableIdElement { - - override val id by links.nextId { this } - - val aAttrib = Node.attributes[a]!! - val bAttrib = Node.attributes[b]!! - - override fun draw() { - if(!aAttrib.links.contains(this)) - aAttrib.links.add(this) - - if(!bAttrib.links.contains(this)) - bAttrib.links.add(this) - - ImNodes.link(id, a, b) - } - - override fun delete() { - aAttrib.links.remove(this) - bAttrib.links.remove(this) - - links.removeId(id) - } - - override fun restore() { - links[id] = this - - aAttrib.links.add(this) - bAttrib.links.add(this) - } - - companion object { - val links = IdElementContainer() - - fun getLinksBetween(a: Node<*>, b: Node<*>): List { - val l = mutableListOf() - - for(link in links) { - val linkNodeA = link.aAttrib.parentNode - val linkNodeB = link.bAttrib.parentNode - - if ( - (a == linkNodeA && b == linkNodeB) || (b == linkNodeA && a == linkNodeB) - ) { - l.add(link) - } - } - - return l - } - } - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt deleted file mode 100644 index fda179ea..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/Node.kt +++ /dev/null @@ -1,183 +0,0 @@ -package io.github.deltacv.easyvision.node - -import imgui.ImGui -import imgui.ImVec2 -import io.github.deltacv.easyvision.id.DrawableIdElement -import io.github.deltacv.easyvision.id.IdElementContainer -import io.github.deltacv.easyvision.attribute.Attribute -import io.github.deltacv.easyvision.attribute.AttributeMode -import io.github.deltacv.easyvision.codegen.CodeGen -import io.github.deltacv.easyvision.codegen.CodeGenSession -import io.github.deltacv.easyvision.codegen.GenValue -import io.github.deltacv.easyvision.exception.NodeGenException - -interface Type { - val name: String -} - -abstract class Node( - private var allowDelete: Boolean = true -) : DrawableIdElement { - - var nodesIdContainer = nodes - var attributesIdContainer = attributes - - var drawAttributesCircles = true - - override val id by lazy { nodesIdContainer.nextId(this).value } - - private val attribs = mutableListOf() // internal mutable list - val nodeAttributes = attribs as List // public read-only - - var genSession: S? = null - private set - - protected fun drawAttributes() { - for((i, attribute) in nodeAttributes.withIndex()) { - attribute.draw() - - if(i < nodeAttributes.size - 1 && !attribute.wasLastDrawCancelled) { - ImGui.newLine() // make a new blank line if this isn't the last attribute - } - } - } - - override fun delete() { - if(allowDelete) { - for (attribute in nodeAttributes.toTypedArray()) { - for(link in attribute.links.toTypedArray()) { - link.delete() - } - - attribute.delete() - attribs.remove(attribute) - } - - nodes.removeId(id) - } - } - - override fun restore() { - if(allowDelete) { - for (attribute in nodeAttributes.toTypedArray()) { - for(link in attribute.links.toTypedArray()) { - link.restore() - } - - attribute.restore() - attribs.add(attribute) - } - - nodes[id] = this - } - } - - fun addAttribute(attribute: Attribute) { - attribute.parentNode = this - attribs.add(attribute) - } - - operator fun Attribute.unaryPlus() = addAttribute(this) - - abstract fun genCode(current: CodeGen.Current): S - - open fun getOutputValueOf(current: CodeGen.Current, attrib: Attribute): GenValue { - raise("Node doesn't have output attributes") - } - - private var isCurrentlyGenCode = false - - @Suppress("UNCHECKED_CAST") - /** - * Generates code if there's not a session in the current CodeGen - * Automatically propagates to all the nodes attached to the output - * attributes after genCode finishes. Called by default on onPropagateReceive() - */ - fun genCodeIfNecessary(current: CodeGen.Current) { - val codeGen = current.codeGen - val session = codeGen.sessions[this] - - if(session == null) { - // prevents duplicate code in weird edge cases - // (it's so hard to consider and test every possibility with nodes...) - if(!isCurrentlyGenCode) { - isCurrentlyGenCode = true - - genSession = genCode(current) - codeGen.sessions[this] = genSession!! - - isCurrentlyGenCode = false - - propagate(current) - } - } else { - genSession = session as S - } - } - - fun propagate(current: CodeGen.Current) { - for(attribute in attribs) { - if(attribute.mode == AttributeMode.OUTPUT) { - for(linkedAttribute in attribute.linkedAttributes()) { - linkedAttribute.parentNode.onPropagateReceive(current) - } - } - } - } - - open fun onPropagateReceive(current: CodeGen.Current) { - genCodeIfNecessary(current) - } - - fun raise(message: String): Nothing = throw NodeGenException(this, message) - - fun warn(message: String) { - println("WARN: $message") // TODO: Warnings system... - } - - fun raiseAssert(condition: Boolean, message: String) { - if(!condition) { - raise(message) - } - } - - fun warnAssert(condition: Boolean, message: String) { - if(!condition) { - warn(message) - } - } - - companion object { - val nodes = IdElementContainer>() - val attributes = IdElementContainer() - - @JvmStatic protected val INPUT = AttributeMode.INPUT - @JvmStatic protected val OUTPUT = AttributeMode.OUTPUT - - fun checkRecursion(from: Node<*>, to: Node<*>): Boolean { - val linksBetween = Link.getLinksBetween(from, to) - - var hasOutputToInput = false - var hasInputToOutput = false - - for(link in linksBetween) { - val aNode = link.aAttrib.parentNode - - val fromAttrib = if(aNode == from) link.aAttrib else link.bAttrib - val toAttrib = if(aNode == to) link.aAttrib else link.bAttrib - - if(!hasOutputToInput) - hasOutputToInput = fromAttrib.mode == OUTPUT && toAttrib.mode == INPUT - - if(!hasInputToOutput) - hasInputToOutput = fromAttrib.mode == INPUT && toAttrib.mode == OUTPUT - - if(hasOutputToInput && hasInputToOutput) - break - } - - return hasOutputToInput && hasInputToOutput - } - } - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt deleted file mode 100644 index a2bb55ec..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeEditor.kt +++ /dev/null @@ -1,137 +0,0 @@ -package io.github.deltacv.easyvision.node - -import imgui.ImGui -import imgui.extension.imnodes.ImNodes -import imgui.extension.imnodes.ImNodesContext -import imgui.flag.ImGuiMouseButton -import imgui.type.ImInt -import io.github.deltacv.easyvision.EasyVision -import io.github.deltacv.easyvision.gui.PopupBuilder -import io.github.deltacv.easyvision.attribute.AttributeMode -import io.github.deltacv.easyvision.node.vision.InputMatNode -import io.github.deltacv.easyvision.node.vision.OutputMatNode - -class NodeEditor(val easyVision: EasyVision) { - - val context = ImNodesContext() - - var isNodeFocused = false - private set - - val inputNode = InputMatNode() - val outputNode = OutputMatNode() - - fun init() { - ImNodes.createContext() - inputNode.enable() - outputNode.enable() - } - - fun draw() { - ImNodes.editorContextSet(context) - - if(easyVision.nodeList.isNodesListOpen) { - ImNodes.clearLinkSelection() - ImNodes.clearNodeSelection() - } - - ImNodes.beginNodeEditor() - - for(node in Node.nodes) { - node.draw() - } - for(link in Link.links) { - link.draw() - } - - ImNodes.endNodeEditor() - - isNodeFocused = ImNodes.numSelectedNodes() > 0 - - handleDeleteLink() - handleCreateLink() - handleDeleteSelection() - } - - private val startAttr = ImInt() - private val endAttr = ImInt() - - private fun handleCreateLink() { - if(ImNodes.isLinkCreated(startAttr, endAttr)) { - val start = startAttr.get() - val end = endAttr.get() - - val startAttrib = Node.attributes[start]!! - val endAttrib = Node.attributes[end]!! - - val input = if(startAttrib.mode == AttributeMode.INPUT) start else end - val output = if(startAttrib.mode == AttributeMode.OUTPUT) start else end - - val inputAttrib = Node.attributes[input]!! - val outputAttrib = Node.attributes[output]!! - - if(startAttrib.mode == endAttrib.mode) { - return // linked attributes cannot be of the same mode - } - - if(!startAttrib.acceptLink(endAttrib) || !endAttrib.acceptLink(startAttrib)) { - PopupBuilder.addWarningToolTip("Couldn't link nodes: Types didn't match") - return // one or both of the attributes didn't accept the link, abort. - } - - if(startAttrib.parentNode == endAttrib.parentNode) { - return // we can't link a node to itself! - } - - inputAttrib.links.toTypedArray().forEach { - it.delete() // delete the existing link(s) of the input attribute if there's any - } - - val link = Link(start, end).enable() // create the link and enable it - - if(Node.checkRecursion(inputAttrib.parentNode, outputAttrib.parentNode)) { - PopupBuilder.addWarningToolTip("Couldn't link nodes: Recursion problem detected") - // remove the link if a recursion case was detected (e.g both nodes were attached to each other) - link.delete() - } - } - } - - private fun handleDeleteLink() { - val hoveredId = ImNodes.getHoveredLink() - - if(ImGui.isMouseClicked(ImGuiMouseButton.Right) && hoveredId >= 0) { - val hoveredLink = Link.links[hoveredId] - hoveredLink?.delete() - } - } - - private fun handleDeleteSelection() { - if(easyVision.isDeleteReleased) { - if(ImNodes.numSelectedNodes() > 0) { - val selectedNodes = IntArray(ImNodes.numSelectedNodes()) - ImNodes.getSelectedNodes(selectedNodes) - - for(node in selectedNodes) { - try { - Node.nodes[node]?.delete() - } catch(ignored: IndexOutOfBoundsException) {} - } - } - - if(ImNodes.numSelectedLinks() > 0) { - val selectedLinks = IntArray(ImNodes.numSelectedLinks()) - ImNodes.getSelectedLinks(selectedLinks) - - for(link in selectedLinks) { - Link.links[link]?.delete() - } - } - } - } - - fun destroy() { - ImNodes.destroyContext() - } - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt deleted file mode 100644 index 1c1348bc..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeList.kt +++ /dev/null @@ -1,245 +0,0 @@ -package io.github.deltacv.easyvision.node - -import imgui.ImColor -import imgui.ImFont -import imgui.ImGui -import imgui.ImVec2 -import imgui.extension.imnodes.ImNodes -import imgui.extension.imnodes.ImNodesContext -import imgui.extension.imnodes.flag.ImNodesAttributeFlags -import imgui.extension.imnodes.flag.ImNodesColorStyle -import imgui.extension.imnodes.flag.ImNodesStyleFlags -import imgui.flag.* -import imgui.type.ImBoolean -import io.github.deltacv.easyvision.EasyVision -import io.github.deltacv.easyvision.attribute.Attribute -import io.github.deltacv.easyvision.gui.makeFont -import io.github.deltacv.easyvision.id.IdElementContainer -import io.github.deltacv.easyvision.node.vision.CvtColorNode -import io.github.deltacv.easyvision.util.ElapsedTime -import kotlinx.coroutines.* - -class NodeList(val easyVision: EasyVision) { - - companion object { - val listNodes = IdElementContainer>() - val listAttributes = IdElementContainer() - - lateinit var categorizedNodes: CategorizedNodes - private set - - @OptIn(DelicateCoroutinesApi::class) - private val annotatedNodesJob = GlobalScope.launch(Dispatchers.IO) { - categorizedNodes = NodeScanner.scan() - } - } - - lateinit var buttonFont: ImFont - - val plusFontSize = 60f - - var isNodesListOpen = false - private set - - private var lastButton = false - - private val openButtonTimeout = ElapsedTime() - private val hoveringPlusTime = ElapsedTime() - - private lateinit var listContext: ImNodesContext - - - fun init() { - buttonFont = makeFont(plusFontSize) - } - - fun draw() { - val size = EasyVision.windowSize - - if(!easyVision.nodeEditor.isNodeFocused && easyVision.isSpaceReleased) { - showList() - } - - if(easyVision.isEscReleased) { - closeList() - } - - // NODES LIST - - if(isNodesListOpen) { - ImGui.setNextWindowPos(0f, 0f) - ImGui.setNextWindowSize(size.x, size.y, ImGuiCond.Always) - - ImGui.pushStyleColor(ImGuiCol.WindowBg, 0f, 0f, 0f, 0.55f) // transparent dark nodes list window - - ImGui.begin("nodes", - ImGuiWindowFlags.NoResize or ImGuiWindowFlags.NoMove - or ImGuiWindowFlags.NoCollapse or ImGuiWindowFlags.NoTitleBar - or ImGuiWindowFlags.NoDecoration //or ImGuiWindowFlags.NoBringToFrontOnFocus - ) - - drawNodesList() - - ImGui.end() - - ImGui.popStyleColor() - } - - // OPEN/CLOSE BUTTON - - ImGui.setNextWindowPos(size.x - plusFontSize * 1.8f, size.y - plusFontSize * 1.8f) - - if(isNodesListOpen) { - ImGui.setNextWindowFocus() - } - - ImGui.begin( - "floating", ImGuiWindowFlags.NoBackground - or ImGuiWindowFlags.NoTitleBar or ImGuiWindowFlags.NoDecoration or ImGuiWindowFlags.NoMove - ) - - ImGui.pushFont(buttonFont) - val buttonSize = ImGui.getFrameHeight() - - val button = ImGui.button(if(isNodesListOpen) "x" else "+", buttonSize, buttonSize) - - ImGui.popFont() - - if (button != lastButton && !isNodesListOpen && button && openButtonTimeout.millis > 200) { - showList() - } - - if(ImGui.isItemHovered()) { - if(hoveringPlusTime.millis > 500) { - ImGui.beginTooltip() - ImGui.text( - if(isNodesListOpen) { - "Press ESCAPE to close the nodes list" - } else "Press SPACE to open the nodes list" - ) - ImGui.endTooltip() - } - } else { - hoveringPlusTime.reset() - } - - lastButton = button - - ImGui.end() - } - - val nodes by lazy { - val map = mutableMapOf>>() - - for((category, nodeClasses) in categorizedNodes) { - val list = mutableListOf>() - - for(nodeClass in nodeClasses) { - val instance = nodeClass.getConstructor().newInstance() - - instance.nodesIdContainer = listNodes - instance.attributesIdContainer = listAttributes - instance.drawAttributesCircles = false - instance.enable() - - list.add(instance) - } - - map[category] = list - } - - map - } - - private fun drawNodesList() { - ImNodes.editorContextSet(listContext) - - ImNodes.getStyle().gridSpacing = 99999f // lol only way to make grid invisible - ImNodes.pushColorStyle(ImNodesColorStyle.GridBackground, ImColor.floatToColor(0f, 0f, 0f, 0f)) - - ImNodes.clearNodeSelection() - ImNodes.clearLinkSelection() - ImNodes.editorResetPanning(0f, 0f) - - var closeOnClick = true - - ImNodes.beginNodeEditor() - val flags = ImGuiTreeNodeFlags.DefaultOpen - - for(category in Category.values()) { - if(nodes.containsKey(category)) { - for (node in nodes[category]!!) { - if (ImGui.collapsingHeader(category.properName, flags)) { - if (ImGui.isItemHovered()) { - closeOnClick = false - } - - node.draw() - } else if (ImGui.isItemHovered()) { - closeOnClick = false - } - } - } - } - ImNodes.endNodeEditor() - - ImNodes.getStyle().gridSpacing = 32f // back to normal - ImNodes.popColorStyle() - - handleClick(closeOnClick) - } - - fun handleClick(closeOnClick: Boolean) { - if(ImGui.isMouseClicked(ImGuiMouseButton.Left)) { - val hovered = ImNodes.getHoveredNode() - - if(hovered >= 0) { - val nodeClass = listNodes[hovered]!!::class.java - val instance = nodeClass.getConstructor().newInstance() - instance.enable() - - if(instance is DrawNode<*>) { - val nodePos = ImVec2() - ImNodes.getNodeScreenSpacePos(hovered, nodePos) - - val mousePos = ImGui.getMousePos() - - val newPosX = mousePos.x - nodePos.x - val newPosY = mousePos.y - nodePos.y - - instance.nextNodePosition = ImVec2(newPosX, newPosY) - instance.pinToMouse = true - } - - closeList() - } else if(closeOnClick) { - closeList() - } - } - } - - fun showList() { - if(!isNodesListOpen) { - if(!annotatedNodesJob.isCompleted) { - runBlocking { - annotatedNodesJob.join() // wait for the scanning to finish - } - } - - if(::listContext.isInitialized) { - listContext.destroy() - } - listContext = ImNodesContext() - - isNodesListOpen = true - } - } - - fun closeList() { - if(isNodesListOpen) { - isNodesListOpen = false - openButtonTimeout.reset() - } - } - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeScanner.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeScanner.kt deleted file mode 100644 index 09fa1996..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/NodeScanner.kt +++ /dev/null @@ -1,64 +0,0 @@ -package io.github.deltacv.easyvision.node - -import io.github.classgraph.ClassGraph - -typealias CategorizedNodes = Map>>> - -object NodeScanner { - - val ignoredPackages = arrayOf( - "java", - "org.opencv", - "imgui", - "io.github.classgraph", - "org.lwjgl" - ) - - @Suppress("UNCHECKED_CAST") //shut - fun scan(): CategorizedNodes { - val nodes = mutableMapOf>>>() - - println("Scanning for nodes...") - - val classGraph = ClassGraph() - .enableClassInfo() - .enableAnnotationInfo() - .rejectPackages(*ignoredPackages) - - val scanResult = classGraph.scan() - val nodeClasses = scanResult.getClassesWithAnnotation(RegisterNode::class.java.name) - - for(nodeClass in nodeClasses) { - val clazz = Class.forName(nodeClass.name) - - val regAnnotation = clazz.getDeclaredAnnotation(RegisterNode::class.java) - - if(hasSuperclass(clazz, Node::class.java)) { - val nodeClazz = clazz as Class> - - var list = nodes[regAnnotation.category] - - if(list == null) { - list = mutableListOf(nodeClazz) - nodes[regAnnotation.category] = list - } else { - list.add(nodeClazz) - } - } - } - - println("Found ${nodeClasses.size} nodes") - - return nodes - } - -} - -fun hasSuperclass(clazz: Class<*>, superClass: Class<*>): Boolean { - return try { - clazz.asSubclass(superClass) - true - } catch (ex: ClassCastException) { - false - } -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/RegisterNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/RegisterNode.kt deleted file mode 100644 index 60a839ea..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/RegisterNode.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.deltacv.easyvision.node - -enum class Category(val properName: String) { - CV_BASICS("Basic OpenCV Operations"), - MATH("Math Operations"), - MISC("Miscellaneous") -} - -annotation class RegisterNode(val name: String, val category: Category, val description: String = "") diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt deleted file mode 100644 index 29901e67..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/math/SumIntegerNode.kt +++ /dev/null @@ -1,50 +0,0 @@ -package io.github.deltacv.easyvision.node.math - -import io.github.deltacv.easyvision.attribute.Attribute -import io.github.deltacv.easyvision.node.DrawNode -import io.github.deltacv.easyvision.attribute.math.IntAttribute -import io.github.deltacv.easyvision.attribute.misc.ListAttribute -import io.github.deltacv.easyvision.codegen.CodeGen -import io.github.deltacv.easyvision.codegen.CodeGenSession -import io.github.deltacv.easyvision.codegen.GenValue -import io.github.deltacv.easyvision.node.RegisterNode -import io.github.deltacv.easyvision.node.Category - -@RegisterNode( - name = "Sum Integers", - category = Category.MATH, - description = "Sums a list of integers and outputs the result" -) -class SumIntegerNode : DrawNode("Sum Integers") { - - val numbers = ListAttribute(INPUT, IntAttribute, "Numbers") - val result = IntAttribute(OUTPUT, "Result") - - override fun onEnable() { - + numbers - + result - } - - override fun genCode(current: CodeGen.Current) = current { - val session = Session() - - - - session - } - - override fun getOutputValueOf(current: CodeGen.Current, attrib: Attribute): GenValue { - genCodeIfNecessary(current) - - if(attrib == result) { - return genSession!!.result - } - - raise("Attribute $attrib is not an output of this node or not handled by this") - } - - class Session : CodeGenSession { - lateinit var result: GenValue.Int - } - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt deleted file mode 100644 index 2816a973..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/CvtColorNode.kt +++ /dev/null @@ -1,95 +0,0 @@ -package io.github.deltacv.easyvision.node.vision - -import io.github.deltacv.easyvision.attribute.Attribute -import io.github.deltacv.easyvision.attribute.misc.EnumAttribute -import io.github.deltacv.easyvision.attribute.vision.MatAttribute -import io.github.deltacv.easyvision.codegen.* -import io.github.deltacv.easyvision.codegen.CodeGenSession -import io.github.deltacv.easyvision.codegen.parse.* -import io.github.deltacv.easyvision.node.RegisterNode -import io.github.deltacv.easyvision.node.Category -import io.github.deltacv.easyvision.node.DrawNode - -enum class Colors(val channels: Int, val channelNames: Array) { - RGBA(4, arrayOf("R", "G", "B", "A")), - RGB(3, arrayOf("R", "G", "B")), - BGR(3, arrayOf("B", "G", "R")), - HSV(3, arrayOf("H", "S", "V")), - YCrCb(3, arrayOf("Y", "Cr", "Cb")), - LAB(3, arrayOf("L", "A", "B")), - GRAY(1, arrayOf("Gray")) -} - -@RegisterNode( - name = "Convert Color", - category = Category.CV_BASICS, - description = "Converts a Mat from its current color space to the specified color space. If the mat is already in the specified color space, no conversion is made." -) -class CvtColorNode : DrawNode("Convert Color") { - - val input = MatAttribute(INPUT, "Input") - val output = MatAttribute(OUTPUT, "Output") - - val convertTo = EnumAttribute(INPUT, Colors.values(), "Convert To") - - override fun onEnable() { - + input - + convertTo - - + output - } - - override fun genCode(current: CodeGen.Current) = current { - val session = Session() - - val inputMat = input.value(current) - inputMat.requireNonBinary(input) - - var targetColor = convertTo.value(current).value - var matColor = inputMat.color - - if(matColor != targetColor) { - if(matColor == Colors.RGBA && targetColor != Colors.RGB) { - matColor = Colors.RGB - } else if(matColor != Colors.RGB && targetColor == Colors.RGBA) { - targetColor = Colors.RGB - } - } - - import("org.opencv.imgproc.Imgproc") - - if(matColor != targetColor) { - val matName = tryName("${targetColor.name.lowercase()}Mat") - - // create mat instance variable - private(matName, new("Mat")) - - current.scope { // add a cvtColor step in processFrame - "Imgproc.cvtColor"(inputMat.value, matName.v, cvtColorValue(matColor, targetColor)) - } - - session.outputMatValue = GenValue.Mat(matName.v, targetColor) // store data in the current session - } else { - // we don't need to do any processing if the mat is - // already of the color the user specified to convert to - session.outputMatValue = inputMat - } - - session - } - - override fun getOutputValueOf(current: CodeGen.Current, attrib: Attribute): GenValue { - genCodeIfNecessary(current) - - if(attrib == output) { - return genSession!!.outputMatValue - } - - raise("Attribute $attrib is not an output of this node or not handled by this") - } - - class Session : CodeGenSession { - lateinit var outputMatValue: GenValue.Mat - } - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MaskNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MaskNode.kt deleted file mode 100644 index 472ae7da..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MaskNode.kt +++ /dev/null @@ -1,72 +0,0 @@ -package io.github.deltacv.easyvision.node.vision - -import io.github.deltacv.easyvision.attribute.Attribute -import io.github.deltacv.easyvision.attribute.vision.MatAttribute -import io.github.deltacv.easyvision.codegen.CodeGen -import io.github.deltacv.easyvision.codegen.CodeGenSession -import io.github.deltacv.easyvision.codegen.GenValue -import io.github.deltacv.easyvision.codegen.parse.new -import io.github.deltacv.easyvision.codegen.parse.v -import io.github.deltacv.easyvision.node.RegisterNode -import io.github.deltacv.easyvision.node.Category -import io.github.deltacv.easyvision.node.DrawNode - -@RegisterNode( - name = "Binary Mask", - category = Category.CV_BASICS, - description = "Takes a normal image and performs a mask based on a binary image, discards or includes areas from the normal image based on the binary image." -) -class MaskNode : DrawNode("Binary Mask"){ - - val inputMat = MatAttribute(INPUT, "Input") - val maskMat = MatAttribute(INPUT, "Binary Mask") - - val outputMat = MatAttribute(OUTPUT, "Output") - - override fun onEnable() { - + inputMat - + maskMat - - + outputMat - } - - override fun genCode(current: CodeGen.Current) = current { - val session = Session() - - val input = inputMat.value(current) - input.requireNonBinary(inputMat) - - val mask = maskMat.value(current) - mask.requireBinary(maskMat) - - val output = tryName("${input.value.value!!}Mask") - - import("org.opencv.core.Core") - - private(output, new("Mat")) - - current.scope { - "$output.release"() - "Core.bitwise_and"(input.value, input.value, output.v, mask.value) - } - - session.outputMat = GenValue.Mat(output.v, input.color) - - session - } - - override fun getOutputValueOf(current: CodeGen.Current, attrib: Attribute): GenValue { - genCodeIfNecessary(current) - - if(attrib == outputMat) { - return genSession!!.outputMat - } - - raise("Attribute $attrib is not an output of this node or not handled by this") - } - - class Session : CodeGenSession { - lateinit var outputMat: GenValue.Mat - } - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt deleted file mode 100644 index 570486ce..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/MatNodes.kt +++ /dev/null @@ -1,66 +0,0 @@ -package io.github.deltacv.easyvision.node.vision - -import imgui.ImVec2 -import imgui.extension.imnodes.ImNodes -import io.github.deltacv.easyvision.EasyVision -import io.github.deltacv.easyvision.attribute.Attribute -import io.github.deltacv.easyvision.node.DrawNode -import io.github.deltacv.easyvision.attribute.vision.MatAttribute -import io.github.deltacv.easyvision.codegen.CodeGen -import io.github.deltacv.easyvision.codegen.GenValue -import io.github.deltacv.easyvision.codegen.NoSession -import io.github.deltacv.easyvision.codegen.parse.v - -class InputMatNode : DrawNode("Pipeline Input", allowDelete = false) { - - override fun init() { - val windowSize = EasyVision.windowSize - val nodeSize = ImVec2() - ImNodes.getNodeDimensions(id, nodeSize) - - ImNodes.setNodeScreenSpacePos(id, nodeSize.x * 0.5f, windowSize.y / 2f - nodeSize.y / 2) - } - - override fun onEnable() { - + MatAttribute(OUTPUT, "Input") - } - - override fun genCode(current: CodeGen.Current) = NoSession - - fun startGen(current: CodeGen.Current) { - propagate(current) - } - - val value = GenValue.Mat("input".v, Colors.RGBA) - - override fun getOutputValueOf(current: CodeGen.Current, - attrib: Attribute) = value -} - -class OutputMatNode : DrawNode("Pipeline Output", allowDelete = false) { - - override fun init() { - val windowSize = EasyVision.windowSize - val nodeSize = ImVec2() - ImNodes.getNodeDimensions(id, nodeSize) - - ImNodes.setNodeScreenSpacePos(id, windowSize.x - nodeSize.x * 1.5f , windowSize.y / 2f - nodeSize.y / 2) - } - - val input = MatAttribute(INPUT, "Output") - - override fun onEnable() { - + input - } - - override fun genCode(current: CodeGen.Current) = current { - current.scope { - returnMethod(input.value(current).value) // start code gen! - appendWhiteline = false - } - - NoSession - } - - override fun getOutputValueOf(current: CodeGen.Current, attrib: Attribute) = GenValue.None -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt deleted file mode 100644 index 1228bb42..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/node/vision/ThresholdNode.kt +++ /dev/null @@ -1,145 +0,0 @@ -package io.github.deltacv.easyvision.node.vision - -import imgui.ImGui -import imgui.type.ImInt -import io.github.deltacv.easyvision.attribute.Attribute -import io.github.deltacv.easyvision.attribute.vision.MatAttribute -import io.github.deltacv.easyvision.attribute.vision.ScalarAttribute -import io.github.deltacv.easyvision.codegen.CodeGen -import io.github.deltacv.easyvision.codegen.CodeGenSession -import io.github.deltacv.easyvision.codegen.GenValue -import io.github.deltacv.easyvision.codegen.parse.cvtColorValue -import io.github.deltacv.easyvision.codegen.parse.new -import io.github.deltacv.easyvision.codegen.parse.v -import io.github.deltacv.easyvision.gui.ExtraWidgets -import io.github.deltacv.easyvision.node.RegisterNode -import io.github.deltacv.easyvision.node.Category -import io.github.deltacv.easyvision.node.DrawNode - -@RegisterNode( - name = "Color Threshold", - category = Category.CV_BASICS, - description = "Performs a threshold in the input image and returns a binary image, discarding the pixels that were outside the range in the color space specified." -) -class ThresholdNode : DrawNode("Color Threshold") { - - val input = MatAttribute(INPUT, "Input") - val scalar = ScalarAttribute(INPUT, Colors.values()[0], "Threshold") - val output = MatAttribute(OUTPUT, "Binary Output") - - override fun onEnable() { - + input - + scalar - + output - } - - val colorValue = ImInt() - - private var lastColor = Colors.values()[0] - - override fun drawNode() { - input.drawHere() - - ImGui.newLine() - ImGui.text("(Enum) Color Space") - - ImGui.pushItemWidth(110.0f) - val color = ExtraWidgets.enumCombo(Colors.values(), colorValue) - ImGui.popItemWidth() - - ImGui.newLine() - - if(color != lastColor) { - scalar.color = color - } - - lastColor = color - } - - override fun genCode(current: CodeGen.Current) = current { - val session = Session() - - val range = scalar.value(current) - - var inputMat = input.value(current) - inputMat.requireNonBinary(input) - - var matColor = inputMat.color - var targetColor = lastColor - - if(matColor != targetColor) { - if(matColor == Colors.RGBA && targetColor != Colors.RGB) { - matColor = Colors.RGB - } else if(matColor != Colors.RGB && targetColor == Colors.RGBA) { - targetColor = Colors.RGB - } - } - - val needsCvt = matColor != targetColor - - val cvtMat = tryName("${targetColor.name.lowercase()}Mat") - val thresholdTargetMat = tryName("${targetColor.name.lowercase()}BinaryMat") - - val lowerScalar = tryName("lower${targetColor.name}") - val upperScalar = tryName("upper${targetColor.name}") - - // add necessary imports - import("org.opencv.imgproc.Imgproc") - import("org.opencv.core.Scalar") - import("org.opencv.core.Core") - - // lower color scalar - public(lowerScalar, - new("Scalar", - range.a.min.toString(), - range.b.min.toString(), - range.c.min.toString(), - range.d.min.toString(), - ) - ) - - // upper color scalar - public(upperScalar, - new("Scalar", - range.a.max.toString(), - range.b.max.toString(), - range.c.max.toString(), - range.d.max.toString(), - ) - ) - - if(needsCvt) { - private(cvtMat, new("Mat")) - } - // output mat target - private(thresholdTargetMat, new("Mat")) - - current.scope { - if(needsCvt) { - "Imgproc.cvtColor"(inputMat.value, cvtMat.v, cvtColorValue(matColor, targetColor)) - inputMat = GenValue.Mat(cvtMat.v, targetColor) - } - - "Core.inRange"(inputMat.value, lowerScalar.v, upperScalar.v, thresholdTargetMat.v) - } - - session.outputMat = GenValue.Mat(thresholdTargetMat.v, targetColor, true) - - session - } - - override fun getOutputValueOf(current: CodeGen.Current, attrib: Attribute): GenValue { - genCodeIfNecessary(current) - - if(attrib == output) { - return genSession!!.outputMat - } - - raise("Attribute $attrib is not an output of this node or not handled by this") - } - - class Session : CodeGenSession { - lateinit var outputMat: GenValue.Mat - } - -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/serialization/NodeSerializer.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/serialization/NodeSerializer.kt deleted file mode 100644 index 903d5b07..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/serialization/NodeSerializer.kt +++ /dev/null @@ -1,4 +0,0 @@ -package io.github.deltacv.easyvision.serialization - -class NodeSerializer { -} \ No newline at end of file diff --git a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/util/ElapsedTime.kt b/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/util/ElapsedTime.kt deleted file mode 100644 index 2bd57deb..00000000 --- a/EasyVision/src/main/kotlin/io/github/deltacv/easyvision/util/ElapsedTime.kt +++ /dev/null @@ -1,15 +0,0 @@ -package io.github.deltacv.easyvision.util - -class ElapsedTime { - - var startTime = System.currentTimeMillis() - private set - - val millis get() = System.currentTimeMillis() - startTime - val seconds get() = millis.toDouble() / 1000.0 - - fun reset() { - startTime = System.currentTimeMillis() - } - -} \ No newline at end of file diff --git a/doc/images/eocvsim_screenshot_installation_1.png b/doc/images/eocvsim_screenshot_installation_1.png deleted file mode 100644 index eedb048a0558e989c3e527690049b7930806c0cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17362 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 Q@?E4d)`#jf>fDe2Kf4ear~m)} diff --git a/doc/images/eocvsim_screenshot_installation_3.png b/doc/images/eocvsim_screenshot_installation_3.png deleted file mode 100644 index 49c9d790319a53fc8580711826e0c3c8d3b3dd0b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18310 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 OV5G$r#L7er1O5+eKVMn^ diff --git a/doc/images/eocvsim_screenshot_installation_5.png b/doc/images/eocvsim_screenshot_installation_5.png deleted file mode 100644 index fffbbc9115468092b75d2d11c4c050f471dfc683..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 120140 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 L_PFGcX~_Qv*uhxv diff --git a/doc/images/eocvsim_screenshot_structure.png b/doc/images/eocvsim_screenshot_structure.png deleted file mode 100644 index b82ab3438e534e6723f4ea27bbce6fd527766c1b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11151 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¥}^l_L_GCHb9sguZt2t)q0RI@`<5bE*x4 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%0A0R|S z+N(6K+Wa$3Q1T;%Wgu=qDBZoMw*?|r`FBCx_O)(dOujP!oRe-CRoWGIwgutK@t-KP z$jg(%m4J{ zTdZle;p{ct4!$c5^o^nG<6+>(m(qF9=q%t~)fWTlmj2vN6gu*8?4$m^PszaIr@^ui zpWM4063&%~#yqp{m8}v=pev{I9}~Vdh9{bWlDpuGGKbe91J(lX`nbI3W-Axy{Px1} z<0k=s{iTYDJ;{}-H8ac!daU!HY3zs9dFSb792sV-`|kO-G|buR#k?o>9A+El#}4)! z=9jyeled`jqcO}K`YO!%>7UMF26y}Kuy`v8`4OV2Ux_~^1qV_{V9#NaUopOz_uP6J zK!*-g?g?c0M+_Vq#DxmF-W-IDe)CHhsDN-k90rBt28cFu(?Wx7goCAyxFpfR*R7~W zFNY`?-@e>ua>qEt*3u0c7@!8GP%ehtJ_@1g4)I~;$jf%shdOpP{WAv6vm9cBHeVMe zM|y%6k|f^j*)yzp~4nb6@j{Cx|R%X4U~21D5Q~I3mNkiOeiK zT8~(il*!z_MFuR{_B2B_17HSrBK=T0LsaAll=f|4_ym}yLoRv-6%FnQXK)3fi=!XO zOD;QrssJ&(-O+em(S3c84F~ee6EQnSG2g&+d&Zz$mDnSP*zai9QT+t|5Owk!*8FcF4uH zCr3*h*I?ZBxtJC7Z5bAXokO&MXFLD{kEff9lP-y8T8n2%iI;DQmraSga~!V>Nl-{h zP-;ogf+yT9Nzj%|#Gb>{R82H;OynO-FoPtTn8XPk--hQuiFbkB?US~zl_9iDdR&s^ z*plR=3VH%eb~{dT=T3IPeyb+Q-k!-`$H}nOo9;KD>#*B*Gs&csB`scIfVA*AVm7;q22%m;iYIpE^^V@bpM*P zWH!)Yj{h*!0dFS_Z_%a#({*ij4fGTcfaV72v4Z;H;I}3bEfblR$EmtY8CuC{L@DXx z!C7Z>S?7|bC)jhCrV*bdv-yP5L@BcgTeC^#v&p11;srsPhuMKS*;HORTD9&K2hre@ zCwP`wadIB?*vO6=$Cyd3Kwvf(5ye_dF8@ic;Lpg8sA-;Xg|^n9MoNuDfS z{#T=H`Ja)UvPo@~>r@IuK}1ve0uDJ*ECp}v1vyWsMoK76q{zL5GTtZ`)a2&yetF(3 z36AN@e0KyVW(gswDL}|GT%(INRiWnBEo8(kXbCH_U@lajr$JjrE?Ndyth4D9gS=gD zjt}@6UFQx}Zzbnbi$RMQYsD1{kry>^mr!&Ukr4^mS(RKO=X%prY{=qy#ihXOPN8N-Q;Ph{rnmwB0^BzyJ$6nt=TeYw$`r=>k-8`npLfG z#FP`oS%MeWxnLQ0Uq@3JMr?nuM1t8B#Ri=TAlAy|w*+y!+oKwAvN93E&nL#KKBRA@ zTfXa%>x{$5SMJ<2+nb*%dvMo9&!c-*;6vuIr|ow1o-eUawDyZTYZU4Gnd+Bn?H2<9 z9K~sF#Jffml8xyK3%03T4ojiaGhd8+z)9kua2{W~dn)Je6cWM+sEa3Hcq+RLUQ~+X zIm+Kln&+ZW-44@_V{F3_F%pIih*%I%oB|>DQqd+(BWOP}T9~kbO?_ofMK6$+d2S)e z*u=I#nb#d$h@;{#_bJ8UyvGb%vt3(Bd+L{Ho9;I2h^^Va*Ece^TlrP9EynRB!`5v7 zjjs~UCGCIlm0iccf90!h`OFbOUku@+vijMF8-cZU2^$sv%hM?d+B&+viM#+GfJJ5N9}ujCpReocOLh2=59%R2x5%MS&DG=t&)w6O@MxUq&X<@8Ff*!eF1rW z>^OHHOF|blTg%KBY$t2wP46c^RbKGbW}n9CR`Zw2)9v=d?bCq(3_rGJOY7Wj-OX5a z_Ko$4+`{ZIwq}c*`c<=y)mMA`i?4pwY_Wf}sLJoVa@E)K)1CeYe#4Egoo|1s{($|z zes;+C^!z-MKDd@p=7eA;0CkB8k9MGokYYqy(={7k*}eWib0_GEvLKhPsLQw7%+oq@&C_FiXMhZ_#Fi5+O>m&vH?-eHKaRX=6Ft1L8NosM)fs zh?b}}ta3W!Y82c{D*l(6?KVH2K>a`Yij5i^{2$hAFZc>uv;E_I!WH4+=dF7gJ5gUq zOt`UpwU>D`{2$hA4WHm9QuF0=ikK=f02ghq(SHi@nHoi4`6_FVOYKLoZV>Eof!_ib zwq~p6zfxQ1#!HE<+3HOW))$$!bN}Ki15x=~>kvwWenF;7u+~q$GN`KGDKdCxsaN+` zd}aD(XtuHSd&Se}vU^|D-n7ntuY`_Z`D*A*=gIe~0Mc@c%j(8i6Lr-v&Btb~v<$sL z)ins$^2@B8b3>-H{AL}S_^v?)BVLAV89#WBNQw_QDZ&f@r!gh>m4;reYD`@q2 z%syi#8nJxEr|496!BcbCPoaX&F9+Ii}FXX+XIl z2Q*OJ;bj`m>)rf>qY>7;(%&Yb?P+*H(ayux@}p2~Y6@9Y(85QStH%Q_b3$>+!=E^A z9uI0W%*5tte7ivCzMlP%5#T{ODsxq5Y>G-H|89m{A#v~eLTKsW7 z-o1Qm*jkfubvRKKn0xD%gl7E7G>+!OBAqQ!?Yfhh>X?S|HoH}mKl9ben=Z1(>Tu0X ztE=8Rc?V>20gLO_F9@W}N}emER-nF?&(9U8Hfn9(M1bl-;>lXX8a-|EJEK$<-ZL}> zKJM9y1xI;ZGKp^_opgHM4;;OcBj&eShID&-({%Z~$@+{G0=Cb8F-Zf>e_!!5YmL;< z;Zbu>O_Ko0S{pazSn~;mIV9#$I=6ynPXF!q*OlTqZr;WyNe=EwE+_77b=bHar}b%n z-7~js6&`B=_xpgtZuTrnT}dbrdbXQ=H6w-ZwpX5%`|=k}wl%d6hImR0-%E`q)R~uB zW}0N)mb>29r|!_W+q@nh3*?DDseZF2Q(A7Ak14hA~Ls zd&t8Hc^T!yUeAqZ zhPi{oJtV?Cjl-enaIe*HpQG>~Ff4=%gi#I;gTXH6!NQxv;h0reBmf=_fW@j{Uw7g0 z60pQ#c=7-|#Tb?Wh{$S&=MKOMx!^@Gc*!cf3=mKnF+^9I-W6nBsoWK&505F~dm!4k*TZtHCQx|rYgfsv6%y_Gq-0XS0T6a9V9Fc6Y z4^Ih01FgrdQ0OH&-)k;6nBmNX95{QpCLaBasx6H8mU6n1bn1?FTVFi1(BVp+U{lY) zCS=mB7Rvi`AgALbxKNUtXEHP;xl1(OYcAREI62`}a)2Z%!~_+Va*Le;`u}Z?PN(6lnPB}1*+A(lUIPC)`ias}uj+U`q zl@c9Jr8!6Om@>)WFfBqjnX)L|1&OaC4>>3bXyfFP6v}uH4P`n^rbz5q18Y3 z7*I@M38(I9X<_7-Xn;R+6Uz?lze_?KH|BJZ^`)zJ+ z@|OQ13w3v}<4>}9R%);>qiV4l#IulL^tItvzkho0Tj2ul`fy^m^##Wo&WK<3xAE5?44#V>Qyq!z?e>6q=yjcDPM{LbF` zvG%ih+s;FdCnkG+#QKRQ^0oyXXLeY;Fi{M}Z~O5rWET1*6k^*=a@+8$-W73Ps$z0- z?0`?u4cKnqHII7;(^RTAiG2*j?2AFGC$=twylslLI6P(gR&wM`kM~AIDgkeu1$Cg> zu@aa0_OnFnZ9k5ya8|5OsEKchrccN`nSR=(_T;-QF|qb?n*eUd{ES6-Ms}nSWg%#a zh8};X_hAL!!P}HpQtW__c#@vJY{Gr=!e`!?1&?ZaJ0mgno9a7?Joj;8UcdF7;Hx&+lZtylMNyqHyf+WV7D8)$@WZ+B!_W zZvQdhLpvtC+{cL}i(zpL_}eI!EL5z!IKJ(p?e9|b&i@+l@z-;8h;;r*7Uvh{=7KB) z{0}~l{WLcp{JwTo2K#~UsheQM*Prc1Mo+SU`;Rz<;&!9lBJO>{jwk))VYa^}i+>LIw6t&Ne2WXN zVf%TQO?X{7l_ZK5SgUmRV!$V1@VmTf(m54hVs*PZmGz$oeAHLiv%@<6IpC9u$03~j zKMnZ&8(I81%!Vb4BDsX$WYM$4yZ3(?@DW$z<=ctMLyEGh%oV2@#ZVznk4bOc#hRPA z%0PbVeBXUaJ;jK6?0}D>oKfkNIEP&DE$n~~!C`mK3bIH^^TOQd4^TCfIBDSN=6ES) z>;5t~BX}7_PBRhaKT7{(ZvGRpK)tI)==Ua5l`Z^)=V^TzFtg@oE~ZP)C? zUz@aRG}hq{vp6WHB~-`F`WIf1#oNT`3$jogvbX+eZf4_X>*8MBzLjp5=3Y(;dhj(Orres|4%k}qv@UccxwcKBcPKVmow}1f-=t*^ zMR|mOeg4r_JeU80flVhK*Vj7~H~}YFBR**WPw@jlAU-au z{7Wv%mXG%%psc(yLs5_<%LBUOfq?5maUOb~*f%N%wK(V#=0vNx%;u8xob>!hFMZ^> zAd61*q*od%!fD6D4qA1^edQ}+ZO53Ar!jTuUo^f*y*(aJ)3q&b#9DwbsnPlzvYja{Jd)||# zc(g}pD!R-e8H6b#nC-BUr3n1 zr~Zani4tD^XEu2@%ug^pEkhHBRm3+WKk~#%Zf9%KrM^DnfxOfiaDHE|n-&%Z!Ic`j zL=>9SjhrOoB7LR9`XBL zl6-NZnJaTpWKG@Wj5&qxK`=)h*K9iL{m!V#Z`;+!o`)td3a((shvuC*4yG>5&5QHH zG5?DF7qU{~19x@rQ=pg#0ncl5#i*MhGl_0+ z4iKn0@b`evpUurtkRbLjn?$gvaj+OPSOOI+)f_C14wg9z4)5`nl?YK#xhZcPqAus6 z0;bgw)C21H(Km*8VCBf7i>?H%?vCH50ML7nP@!E*a};=U+=91}z!D7k$_dhehCRZi zXC4Sk4+LfJhCMY7e|0DDQee0{ikk4~8eMa^T~7GCEr|GtCy&`-BMIiX2MHU1#bAs< zux1zn4U0d5MI5mtK;d{GILZMYR}4>YhNm3C^U?4`XhgO|L?J37vpJ%GE207rS$q_c zV;ot^6y)bENTWFJ-Hg)BM~#L67!ZT<})hh3pi%;D0<;2X04c)xVS-;E`*1L1oj@QwosAPB+~1db$vtOWs@Lr@JOXe5!eB?y2C5%Fuc^kdaAThUkgPw=4Rc8xA0%ENB~BE3GsQEW9pe};$sI3I5}W4|A_GaV z`xYhdNQI}D;EGFsAC{nD!i;mrNi&75RLmdGGR$)VB-=t^>_J&>5jvQhnBwB=bjbf` zK$f}N!s3{=+!9oe%aCZFsK$X~$PJ>NB`SUuehHbh+?YhjY~dzJ=Oh_iyOgK{q;eSq zsTre!bCM%|l105j0^%oGbh4%-{UVE!+gKZNG@J6PZ2gNYN?TIP|7+PgC9SpPciCED z0EI1E_n4&5vZvR;(_0S(hmO5bK+Z9^$5$(VedZ)t%M3YXkmJ$wi8*d~iHkNjSjBGq z5fdx~?Z~+=7jCbTXiUzsv_?Ln>hr-d-D5X1_Du$q<&Js@winI0{)J+f+x}?q`nqb? z6``yQlWgOX?77*jpylkkmFJg+I7l95E7xSu43Sf(=3G-_r$5OtugPJe%NhDVqsQM; z<8P_)x77GsYWyuV{+1elDK(z$JE>}mfhL|GRNp>%)4TvPS2G2%fS)rv@VsPlC)5dP z*Ie=TpM1Z?;c!D85-!JKl9Z-~2jimX6*=9Q#4dVsz7x^)`SFO4m!C~ui$#NiLt2G+ zO85qw=Z-G;jBiKxsE12zI#BQIDrMb~$Fi@`mX$OY)3|Y;1X+IMWdmm1kdl7MpWC6v z7g9q-t2Qp#r;J_%D>Y(j6N>&MHQH;=@HpByT)s(X^eWh;aFE>7qOz3>UMY|`#@KMO z?cV4`*vVG32qTLTL6Iz924++Ee3hpB!j09q3tS6i?& z+1)q(_{IT*&;Y=HCN(x*d9q-)Ls2)+Jc1rCd0$tdR?YSSGW(Fu26=UpneIFjZDz*0 z3_Lpl$zqz&gJ4)q@H^&UJRA#o6j%xCG7>`w>_FmQLNbCtb;1F<(9p^~fBc+KV<`2} z<$D(BP|NO6Jh?F8CZ9|>JICg*t)j3e17W=@VJ?7hLFI6~9RFv=;m*zBt^?s-=2I4nF81&hM0hKB**5nS+CFziBVz~G4zu#^FK8Wf(n3d=>q@>Sq@ z4iVv8Vdd6#@vFYNyKt=Rs0ByX10oxt5vj1q9OHR1Y+&4jS2s zifSE*dTksz0*D?fj(W8kHN%DFj)=-Bp#=#))NUvtZVUl)48B~mmSav=V^_ck{Nvbf=vV?t1Yt=mF&sh4jlelZ z03k>s6C{-<@-h$!kwjkQMlv`e>49-1C5U-r2%B*nmnUZ>Dvp1Nif1qmI)TLQJO$LU zZ7IdmU}_(n2L{vC7;raJiJMU5BV4;Y;?|eE9?~*uN`OZD$&Fa!Y2_{O1Q`(o9%ph1 zr*a^g>4ZGX1U96fTX={!I3Zjo5d^(z1CJLEOe94nJ@f>dA16JNOm;I-7cjYy8eS>S zynjlK07-c!4jfq;RG23UmV%0C`H!TA-(h_IPpNS^2&b?lReLUAY%&$?kt`vf2BBqa zETNnXr&7VXj8}?bEYD+nJKmDa}Xp7o(as+P`C~#9V_u$sJxH!zm_yqrPM(S^q@wdtN+hqK0GX6FhzfTeTzi2W_e|As?9$Gr0 zh~KTRxSGuN7P!^V{!>&) zF5j1eveL*9`LZW{AjO^{cpf1br8%_>`BK>@#y}tQPPF>kDLC<_A68W4Eiqo;1cy2z z{&z+y=2>nM?|;cik%_e9q@$>_LWY=net8O*Bt|Ed`nRZf`tlR+x3ncH`|{7qBud>P z@&oPmnW*DGM1}kZtf;7Q&Jhn=QGKK?fDi*@6X0arF`IWxJl}VaCq=EK8&AXc_)Cp` zj5i=tXA;7Gbx^9b#vA|Wp!_K#b&4L)I>=(fiV8{7IW;jA*wYTAk(ru$cc}Z*TGDt@ z-vo{iPJpI{`@FOzVLtTSLM=0Toq9$J?;w(65zSlM&&3Co<{BI2xkR@EyWYMZofPl- znEU1Rv^O*7!G4$W_phPWo6b>=X|FiXJN)lE^y3vZ_uW^1GU?{?@!lXPhKlT&-Hp_~ zhnUv@QuXt@T-9f8&6$g@HxvkQNW>a8VufGkNaP=>$-RqQX#`q$91kM(X z*|&P{$}IR4`hbg20!hxD3aD7fvibw&vCB~{O3Z;9oP#ItFHRBkUvy9&uuj~5VWA{L zjx5U>woAhe z$6AUPy5Vlna02FV0jqG&f$;U2a3~fhB*FuXVZqJefhw?YE*!4`m>;GXhJeAs!0;Fp zEUp zbM&l3)Q8pR#?_mtGxqU00Y4N$>j06aI?k^HF}uwH8!EBe4xlYm>>d^;ps_o!SS(I_ z16<$)p(Nt6W9$hwDK&>6f*^=f5I?|3GD#%J6N#;)Ty#*l5p-Ch7(_CuA{kAP%${*< zCNU_X;Old7lDU!mp6r*v@lSE-2b8fHsTKyDSC1tnS>tAX@o+=2u7Y=vLJsRHKp|B| z38~~h>WBOW7p~%g1`zzqRd{nI2y@`bbA#}(t|Fd#?~8|#BS=mVq?Q6U9Sp3kNxD@+ zd4CY}gC;pxC(#{?6D`SU`0+l+$pH|QAm`7Flqc%CDDV=w6YT$zkt#KTLR(TRk5jAR z0TnGNb=+Q8^wNsHBtOkfGdkp745!utQgxR+!xsv@*`Jo7n|!H1J}3~sa3xGYBHdk{ z(H0gW3#Vp&na&WHR$i1|<4HSj;xV8E=M;8}XFp8yl=tQD&M*aLgpW&n4W{TI@ZQeO3>wHri|TuZ>5tevAMT$fA|XpX%)%~ zf3drxE4In^J?XGd!Y?7zRfGQOUUSFoO7q}L1R>0qj|W2cceAgv1qcd3ewqqg>rdQ& z)Gsu6*j=$9DY80L|35-fsc$y*z9S?sZ04Co?(qY>Qz3yPsHjbc?RF}Gtj!S7#zOFp zG}n+R2L<)eQyo0t2lBLKx1L>KA_wNp*gG?Emx1 z*{_h)*#hgomeYrV+5c%{`G37%p zkSi+S?H7n^pT8&gf%mGbFBb?1hWQrSPrAkx{II0;HmFp1I@KaFq0;2l|q6V6yCecyYixgwU(GvsFQ_$#{ z=ICW~^v9#<*5aG=@iE`RAy1CNoQy$RfY`4p7v1)~=9q7*F(;_lGlvVV_yLQ>MxyXc z5csr+%c=;VBf{ocEb%dd^f;EpB$jdwK@LY?LsAeEBn1#jwT7fQMqUNR(PNA4+;Lox zxa%fy+=FpEaO6{H%rl{2VUu_TyLf-h5>+P*yG=|lG>9#>v#HlQ%gr$ZR~VJBRgCZP zKHZf02)700`1K`kA6mwH5+M9(awEMsu(0KYyI_Z&;U-?Vi-bghWa>kEs1X-j0S>n0 z4rQaq-#nFG0*Bu6tR{;FT}*T?^=U!=2Q8yG)L1-FbK2hQDD)PGXH_(w=eVj!Vf zG7Wo?LL>;M)-eqX2^xz_TM0)ECwuPVrc06gsmLYrb&ENexc3Zld3rt@&XxQnz>qeY8h((enOcZ+wE3iqL`4My16NkIVa{Iop zcc~vYXE+|vcHElgat?j5mZT)v-nd&9Il%&nQE*y_%_dN~P39`~d}LI}^v$Nr)7>iB z##0P+Y@GDqCaRxD)Ste@jE`!ms9|7M$_rQFcG~E$X|aFHj}kF`>+h-x+4jO~;UCm5 z>+WYLKile=^#$X)&;^8c7?3+(G1Sj?1MoNJgfZnN$c9kX&`v8+nNKe(72Hsv^apP% zC%ZmmuSUgiq=d6NbtS4yL~b?cr%>qv5H@jYL~IZ4#6h^aY{^R`$mJo03Umq5x02$O zf^W>Gm=oDS*yUzJ*9s7U1#XbUkR0G=J9ebaYOMG3Kq6CjzspoVTdyW@vviE~!7?azNgt zr7NSc>VATs&{&+f!KrEl6)j|0W)s~28~17)(CZ;V)IGw7Ji15#6I{-0f1R8gubM@c z;xkK5iELW^%i1;)_L|<|^suXCN?u4@*UX-WoRcFhaYw6zOsYadHADRUj(r7o3SqBl zYHEQvPC{NTN_U%B131p<>ft!T7>&}`1pq2p5buxXS;cIj3T+-d_K7koJ{e34!5m?C zQLS>3d`IuN{7L$R)t#bp1eN zDm;b208?k0{AcSF%*?pUoa})C!@AOv#&J1s9`R0fKfpg45R*I zezn+t`3jPi%7zfCX2FxRdOe2^;1Ql1yH}GkuFwb~C>KI7TQdQWY_;t_d}7scMmVe5zdU6-v>_HF~7Jg~Xuw5sgF zfe^?iyT@)>>;x(dY(Coh=agNeC%vHLXd%Utt7Nm!*anwQ^$aVErZ`JM1W&;`f^s{3LHyt>uBT^ibYNgpJ1!V-QG)G*SIO@ zZ`bO@Zs%|eDR3iP=I`JTWxoj^Dn_h#Y=&n@lBPnrLCK1;6nEooB)Ff6bO{aewOt3r z&xQ`p3Ms_lsb-7|TM?re?pnz@a6@(vy4cRDgKg^=6qP*1al1H;1RIKF36CcQWFEoT zr^mVOg|&U22`Wu?1AJJuR?J8pHdX;-yyh)N)^%%A5`I^??48Kd(4~)Ko-x8r>n37& zwCz_eY)Edc)`t>)PC|U`nn_v6;@j^m?AN!S;61sgE#H!KMc2`Cjumbt~JG zepg>x#VVUZU{3Yp8oaei9JD1Un?l#?BO>;>n59nPOQRdYW=uLJGyxo>&xmY}c<Uo(Q5UYV!KaPzOb=CD@{9{re5|z(foK1 zPa}o@J}+sy^AJYLDUjfy_j9Tda?u_q2?-GtZ3?wp(u&61TLen-zOc?idtM(%Qh}i1ft5iEU5*~28nQ2d1eqz zJa3e(fDb002VVrhrxp%DLM>pbONupWl$16|^L{HLxnL4&Crf~mqbw2XqJM766K zLJB0i&>rm&wg8k(P@>5Bt61v{)!f30FTZN z$d`jh)U-BOkWmEOZS-P822Xc^%vMQdA6F2d0Diemo*Aava;}yt>p*WrBJX%yF97C&NOb-2Rq*qZ*w^~hxL!Tt}ob7xk z02XM!!B9A$3EK>}ne;p$PZwcu{TX7e3m@vj(kI+&s6$3E0f97KV_mbD00wzhtwwCz ztkFUTpB98`c4Q1mUJR2GUd%6{DXlszQLGy_5t-(YY@_fkLl--ScS}o74EZ-)$!A88 zWCrq3qsY)QH^kkAK9)ce0C9~Ki70FkKPp?M^v3m7O2O$OA9Yd~1(0EPa@o5Esc~E; zv*U`CGYTaGKxV{kkl-glsTOyt&1C{EASMKbmw~hY)FP|0!G9!wiRl&0Om5>uzv$>E z*5tEA`8=TR8jn8FLac!*pC3Xutk^;!Atw-xB@mcoSq7L{3P3wj0&JTde*Lqil> z5K%`LKz`?R6h(Z9V_!9P8jDd<^`#IBU^nnFfukc3qhNa#L0tg1Skg2sDFi4c^Ik3$ z1-UXb3V>N$RA~Y*3R`d=!J`D@v<0c7d5W?HH!~QL#5-A4eIU^jdsY#&12fbo1!>fJ zYEyHARuK>gHRfeSsJ9^h)lNi#J2Ch&JA-;buzuDPYy8GgPh>?AuvZqba_a$KQloi9 zWf-mYPuoWpH^yYeQwB=FdpR*37IrpCGFTY@Q9Mj$8ecYp6@fK&s1U4BZQ#=+TcBwM zQ42-3Jn$7B?t&+aL0=t~5U^Bzb@ftBhfNZJLtL{|40TCB(GIMzJWS;>KH)OWV`1#T z1>AI43WfkO1Qg$<5M=X*K|u<$CVNc+io_>HOXY~3B8u=fUrw?-@^O2+(}^v!Rk89s zMRpM~GY;Yi6!hhY5g;>l#4B&JU=9I?eI^kn*AbcVR?#Q`%_Cf<#}H?eIZVYM zVHlG0GOrgn3^g4=w<5cfBuHp9wIdOpV^KuqkI*s^C$ndWQ$nfg#xe{BAQiiD) zh&6#(&LS_U!CQP)eax5O}M)T87>IaQOLJ)%!I!WV3KzT(zM}ryo zGQcApF3BYLqx4L3K25!r9<*o8uZLXiSU4Y(i#;YShj zA@>CYkar?QNimc0S$m~BI$@h%B9#q6BgvwYT5^(XnJ));2bvLq1c;#LC51B_TY{sJ@=O}4 zSTSjwGjS?8qLu$rmJA1%cobQ=5;}JTF8om#xI(08GjNDBQ5n)il36vt83k#Bk`R<_ z*%m0$_ZDf`nxybnLxVttvM?NDTC;LKf7efXhh_+)E!Q*~>(UruIc#-0h8Kfb#uTE~ z!+5#`73C$RlnG^}*?5)zrZ_HzfIw+4+T&6y;ixiNEkUFq)1f%D2|(!LsVsLWk(p7A zLU~3iD!}P#qz6R$!b8Bss*t9dRP;+gx)DrsWl2G+VBv@O=q4YrPIH=dHleGvYAdjl z9>toh%8C_;fJB+PtT~AkmYS_r!he*II&2=>J&XnH212M2s$H5A+PKNtP)xirV6Wo5vM0HqXg>`01L0> z;S$8UIE3>t|LU$HA)@a}o#|S!(y^+;u`X!Grz0_~{yLebnx)8>6xeo9^$MU8D;7S( zrXOpva}hq0>9H99ShFlaX8~g`m|C(0%cl#Q5#PEOHVdYFTC^bBvoV{oOq&%C%M?%B zv{E~@R9m&ixf!6EsUt@*pn9%e!I-rPJ$T_9QEL)lyAo%s5?Cv)K_#|Zu|nYLRB+q2 zvFf!Wu|X?@w{*L&XuG%L>bJ=w6KQL;Re_$DIB3U1G70_qdI_xMsPW7~3j{ z>!OOQvWJTk?|PupCb^g^qVD9m86vt7L%NQ8xs92*BpSJ{3o4KcyQ=$nm|MBV6r-if zxU$=#t9!Am+q=38yt7-mq{On7Hn5RpyTMDl!~46;8@kJ@ysvwm(@PlDyAvI%7wcTf0WPLq7Yw!VA64YrL@9y)Apb?rXj$E57Ngy93L<^LxKh7`@=TzxIp1 z^1HP9+rRG{zV#cx2Hd>gJ1O`(!2El_{>#7u48P$E!GKu7DXB^AniLT{!4jOXHIczA zQMLk`AmNI^DzU*FA;KP3dJ3a!&1z_R@}e~JjMFEMZ<>pag`j&mki08OsAR5$(H=dm<-CBoJOQvk&`USo*c@m zJjz^^5sEy>eGJQVJj-)j%d%|Ca(v6WEXVY6%as?)ck3-+9IO?R5N=`2$PAcmkrqJF zF6H9PwMKfc+|14FEmZN!%uLI1>&L*X%-f93e_Lxed_l-7x81zV8oR@MOv{QK!#9%4 zqtYTre5%lF#^W2tWSq}w92Ydpt*==jef$;(8PHVQ&LW|=-n`D}ywKb1&Qk%-46V=( zJ<)~ButH41S$xG*oY5;h7B$?$Oo7h;3?vvK(r2vC_I$KoywYE)&nKP6{mjxh+tNJ$ zOVcrJ(>wdpI(^T?Y$82P(S)|gAtBUuLDU;zNH$^8l9ls;Hx+9UpQQ^d(+l59A*k-NFf4ctQI+kv^7Ab73itI{sb)6ebP`wZRFE!{h9-PC>EGrb<14SeJO>!apU zJVGJfE>zy2BN5{5sl~J2P{!UF;oh2^Sr+j;;|+y$X&R56p4%PV$L%vecE+E43K)yOqTS!cry@y`vNdMGX#ZE}fz`yee6 zo1JaX%#^e}9>utux}FW;c=3Jm1JW}=Y6PC7Vq6yzuCy7!Bm@GVBN7~IOojKwfNbl_ zDEG*z9OS4R*O2T;g8b$H9jkPH%7b0kZVs-cT;MIVr>%D8Cb6d)0^n)?&c|x5=8}zO z|E<@Z4CoXx>3hrIFJ|X_#prDO;8YG4mtJ|Bm`l-}u=ZS;vVFZqn#~0bnfP1 z-REV~J2Y9A4|%Gt6;z%{dPe@&v#l>m*%Ntg+-+e*tV6cT49)z!I2VGnI5N+$IU$+d z>=1k7T#VhH62xHM+9ISIk;yv94NMdQ8aj*4#y%7=TRoiK>DxyTrlsZToDq=_@)JQ!Rg&#b}J_s-06lOn|44*nqOCjZ_Om>m28hzm z;?Og&-4c+(ZT5aqXM@iXQN<*;-bE$7@D5d4Vp{xuJZ79>B#Ew6A!W`){BSDm!OuB3dmE_lkVv&|5d(iCK zv`^a>WQ$g=+P7opy1fgx?peAA*A!F}27<+-NPKFoNX8-Jpbd2z>=;3S(u|S_QZ^V# zKoYG{7#oz-Q77djm=8*lY!LHjf>jBYw(Pp#V*#}PbIyKPli(&PNNDQ>j`!gjF$BeA zfq6HzfWHQ%a!t$9szaT%SZN?=*)G8?teg^j+)fKY>EH|2;tjyI08*O-jQS~?s70vo z4cf;{${{_xq)0hsZ3+bhU`0Ox8!;`jZ zthUMAD`+`~)SJ&E02(6^qN?D->_P%bNn#5r)_X{%?{GUvzY0lW%^(0iGq551E_%$Y z6$u)p$nhF-O(#-X5x}d5jQ>+uuJ44)4fFS>bvrjL6xC3~CEzbtCAzcAh&{FH<|DwjljTVMhy9CnHY)-Nq=7pdi50jv&Px zO2WV+r!yIk%0v}%wcgL>tw;p(V%t1#mGL|{Y()fu?F;$EywTk_@BIo zwqk{-bm~V1M?u2+?8Y6hcur}pn6jd(=D&ac{|8_ItumBdg-9oMyHd%b76~oz%_6q> zo@#jYkk~W?Oi@V9_mVJ_DA-K@fE}{Uld=Z2ClzN?k8;S9lwv=FOzt3?BZy40H5QC8 zgjvjrU~Pmp!GdUKRzy);6i}k3nhc8|8f%CP9U`40WzQk?7=yoP2)yJZKoS?40K_bl z1i}r3CoEIbL8w%vel^5Et}zO#wiQP49fV^_DIk6NA^-qHVQW!n#kia}ys>l(N$is6_{cBPoqNJKlPVIfTH_9U}V=3N5V-fPHZ5KTxS3GccWCS4{tnIs?y zAM4>94Z;*ve$bV#gk>xR$Pg*=Wt9n($t4T-5Dm@8lQT1AgW@wMSIwp)5o8Ezj29~> zM(%+Q`pIvmxSfNTa9dvgl;jB?0)gk1ge9e0=0lQDi!q)Gh7M5*MU;3D0K}*wS}0N| zGsl|W+3+ICn~YLe!7+EmOLS_~9s<8c!CJA$ao&Q6?6jAn-|5OApod8y`s(1?TA>ee>Mal^tLzYYmt|=r(8lr$r$OJwS*o1y0!wT0> z1w^A*&6PS5Gl-0$Xxc)617lSZrvQO60%Z_EB2Y1;v1e5S9UzEO_dHp4)vI51<$9in z7~2f;R0XjQz%=!c?BOVLJgG_?Co?l1HUu+p<4W8fk{Y_|MX9#bMfUZXZH!i^(U2(_GkLWwvBGK3VpsR&f0z!-8Es3xHsMU)8CBN+Y5 zAbu~gc@lLACOgeoNiy;w+jMrdN6GqxAuzR>Bns%6{z)^mU}4gD0;jL#ShG3f z;c8=~Q^4oeHI?^;rbXtNq8|n&IE9-qKcld#s;uxKV3GnqOVvg`<;9HwI4fHNMUci? z@+gX*sN5wwvG+e zqpVTOxMnrAwGCBjE(JUvDmS#r9cXs9+ui17(|SOqjOD{$47wuiY3?$OJnQ0?IX8Gc`V2uE<(xD8Uh4J0ah!Vn9g1ic0dkiZvAD zwaXQe;SzGk;>b)_rsH0q7ju;#Ja{E%=UFr)a)`HttP?^&$sgk@z6gCLL{_q8%+yO; zhB!$>H_6G(NA#f#y{*e6`piGhav@0_*EqKb*wqqrj)#+u7OpssE>4o6lYQoI*Y>;R zKKHNkTP#dR)1{D5l{Ujwn<}nN7z?W{*QiP-453XFMl$HZA4nmpa-a~wV;H+Er|<>Y zpeE0qoW{2}?DRI^o0illyI|b(E%__mRgY@`MsoE&-tVl|eM`ORZT7E+Wn6H-d*1i| zKEGGj_`$8xZ{f>Q-~=Bu@i*@Ko;POXyhQrZ*P8K~=e+i<)OA*;)7T<==zrWd$)%(A zV%V#WU?JV0+VL)-=4(A+JnDYcQm%dcdHns)WEHJIetrIlh3oY9y#N$I0Rp{uiyQG9 zK>pjM^OLz~5!DIW04qUc%qd^2xK@?oUQG&r=+rb0`!XQk+hzmZ)DnRqNx9s_hduutL z!xbk4pB`Jj!bv_@aXDOjiSmoPXOq8yGamM1s5QYmLBSI+9J}aYx{7PRHRQDa^(({b zqZ2davpIwdIUE$Nb3^C5!#&BmJbXJdoH;f8z^WU>xMK}Mq(GPp#580?G-N2XD<2S& zzKU=<#$qNY{KF?y3*ZyNPV~h7Ng97!yq|!;*AR;cN}9|eKJrT%@=Fs}+`r$5#Z%k7 zz=Oqx$UM8q#f3P%gv-8WgvMyhuU2c87W^P; zoDOW1##lPS?JK+8D@6vh#OvF?O@yohY`%)~kX|YZb~Lm)EDHowM@}_INf!5GBB5ahy{R5z2X$)3bX z^in|Bd&wUpLn&;@-iw>0bO_0s4y9}er&K+r#4k%5$RZp@0OSn@@fsDenbj!B%!@9&1f4oDmBt}Vmnrk+ZA1EL?<_%e2hQtjx`%%+AC-%;e0^)J)IRywEI7 z(hM@xgtRGpOw&wF!qO(mKqk^SD68sfCh?Bt)0q#Ev=8t>%6su{|aBup<{K6FNv zILbV-+(b~hyBGn`45dI2*`&?;VTk3(oYhQC1%XV~1ku_o(Fw&r6jjj`WzVvlOXGQr zj>wW3jl`>yCQiIh7Ih_=02jhZ0w&}URq+b1pu7bnzuR~W0rLreTu$&zPwdoD=5$gQ z)UPQe!tInvgdD)H{E1qUq6Yn=C{4yAF%YM4w9bSN?+Bp(Gu^VP6jSqj)Bem+tC-Ih zHNO9uQ%c-VICM|Bo6iEQC9c_mEFqO`N-=+w46%ySsbP(4K^ElHI8FF27%e_KU5Gx_ z%}QO=P36>1P0aOZP}#__%t*AV$Q)B`5LHpd2C=-Q6ed@l%+EZqLz9nLWzAZB)!M|> zU8U7ty;WZ=kz9pMUj5Z#4c5P!R2gxfy}T*|6IKl^&CG$1l|re#inC1h)NIw(ZC%C? zt5d7!2#(P=nTW4B!AB9PLID^D&`Pn@;noyQ6%+ZM>3obN$e^HDQsGOyEExqE`IfMB z#ld{nffZ9L^+AG_8Yh~^5PJ?2qS43tCvB3C3h^2L(%GaN!xz&*Sbeb(MC*`2t&)x% z9l4Us<*3%q+>$I2FihRpmNiJH$3L{Tm9lQrgey~ z<(k5ImnbMR$vBcx5K^r@tL3m&*Sah)-IR$HR=k}!z4hDs`P(pU)3hC22UXg`t&qfR zR|^r=9nsJeJu$$uOewIBEl2{-vMNo&O^fiMvGv@~1zoApN6ouWNeBZ%Q>;*U5(-1B z$aRk>klTugB7%w8H)Sw0azJ^rRI5w?uV4xPqtd4s5)lFI+WEk!&_!OvG}xwDUY89y z@k86QeNDw{h>B)@&f}&9(6u1rOQ+1Kte`5!_Hc)>oZX z*SO&t#^Kh~RU78vVfA6W&EX#2VIltEAqL_i4&o(N;wEOYv;i5OIn|$uqYU1ZNfoE2 zsgN8}&<^(EFFsJjXsrLMh}4h-DS)&8F+h-WJy-gq)afX((Us#N>#;V59v7YCt-xb@ z;6`u{@#n8eHGRV~g_~72x6kiUm z(CmcJWzp5S9VRs4P3)zX27*%#NFFz7F@1rW)*hkY1Uf@gtQ&*&|rl;Al4*k zg%rcJ*4kvuT~_CHHq0OafKw`AEyayVrb{~3RIMCPtGq;g7T9|(*=X|C`cF|ZF zD@Oijc2?+xR?H>g86fNA1+C`)yggioKFO|%UW?Y~jc#bniY0-b8j#*&Kep$Q4xbk_ zY2!3byCi9jcIlV)V%wPC$W&ZphD~eM=F9xh!WCv3?%P+T;h=6!y!Gj!Hfo^`W}qJC zr6y{oKI*4VYGRgZiKS|6w(6--+^e2wtv;3Uh3T&bYapcHDxoAXL?M(e>9meZgSO}W zDro(B>y|$0xDM;Owrd1^+SZ9JCW*Pbd&|GBXR`!sxm@YNR_Ug>LzbRv3x*TLo@GQ1 zY`Jvo!G_Dbw(QG(HvnY}#_>c2_3XX)Y}gBJ?5jx82JOZ?Oen?d)n;v}2_v7-vGx2Q zw8PIhY-@dm)3Ai&+`dZx-ey_l^lf}@!r^{nX2mbMZtdh&ZmJpG9(fPy$qVf4HNaAB znEW;gv`8%-?RV5j(%x?EK5f&s#_r~B0F-B(wC?im;^k)V_7))ZdeK^zZ?@x+a%5yo zweOWvfZ3yJMk2U@XyE;v6$%v(L!O<$t#ucjf8R4GjXrV@pR_R zsJUWR$3$&Ym-SgMWvI}PC7CMbuD%sP+B-I$Ofm??@I1TvWjC^n z8`-jqHg!y=^=3!dF`^8)iX;p)=c6HHQTmFyF^?#Sgt0oN4myam`EivDbPj(#8+7(` zSKm!H2&X6=;`F)+fx0LNRQRS(`7n*aBK9Ydw(AChvklknIFa6*#pWnVlI0 zN<`>VKu-ai1df5&HRi>wVJjxlB57e-gi=^y)VUQZ-n@E`-rei>FW|s}2NN!A_%P!C zp{$H%Jekm8lra7btzey)_`L*Rh({sTNyxUs9a^2FWQ?G9QI`-_^xBFcyfYd#Zq}nPl2|C!Trgxu=XF85a@)egbt6p~yufo&aeIG>c5X zF%(~h{4JD(6ioUEQ=xq3S)-<%dip7-ubH-zP@KYt&?rU1sStfeMj_;yQMf6SXpx56 zX`Q#~x+|}}{`6l$T+Vc(YA#81Y(nZFMBav@wbYYG5%lyG5OabhY;nvs*Vqa#i$Ww(DBYfS)AB#LP$%p|9EWz`4BEs0%9i z@<2Gw{BX<=Y58NxJ^TDK&_N45^vy&YeKgY5L2OjN0xg|1)KN=4bzn^weKAvEIV{uD zC(_v*)nSW0HrY?58COQwh2@9Jj#xdTlHA#43^$c@F26pgkZ<*5)$tcG; z9u9P-`44Eq_?OFIX+v|dmQM6Y$3YTuXhR%e5^JKoIEqLB>tR?D6B$0s6|$02OF$A@ zArMy7LL`DTip>VHAJWWfRUX0KPCjYLRkHG3fSipaTPe%`StjjmOwy3tZiqyk^iglV z{30zY63bu`6I4&D3fK_iLyIi0n9X!1G}-l^vAi-ctEuA5E$o$kcbTDl0fjnK195d7sc5fVpBQu3Yt4Aoa~lqGdiaUm(&rtto$ z(1r4eoeXu2E*~n<0pZay4&2T%C#unn0%&^<4PrQ%`9F@5w4~NK&ZXqmOV?bJAlx%4 zOk;W-E{Ra4Hoa-djss4a;k2hdOq!ouLN?0 zQC%ul6VsKsc=0+%QjJcon$@w&6rq_yOiPWb5b#j{L?WbGjlgCWuql$Yu4DaI1SZo) zSRx=~Qm733G*LrfRxUrE$zeb3s@TAUv8IG+Bu6fa5J{X3hlEY(Qx~h*pH|Z_ZINsm z6Y{||I@2X$-7IQ_GP)>CW)yoZ%qj2kS_E_@hLaJ`U?+tc9;P(4zRio)BGOgnBvmOr zdCwSr(-$DjrHBkk6-1WgzAZ#yxcVF+#eS<@w^=cvgCW#KeiN3=st%V6+3Flix!v_P zB`ysyNoZfQnc^}e5>z#>xhhkn*Rr?2-n17kS6UnC9ST)skV153fiEPGXgR;B;Z7BD zTK`gbj8FS$UNSOBWe4Cx6tJ4B%l**8T0K`kGPCS2haZ|EfK%gAL2>XcC4ceetO z1xM!8Rs9U9yWVRM5<1J`A=?BBnHa@5YRsJ|tU>@y7Cp9_^tG~;mwP$b1owX?%EC;PhnDXpmQ-g(bAJP6S}lQ2#($Y=d_~8R zxCYCh&C}MM@r>rms#VdBezb9+V_oTFG_-w=9Ht?yX;%9AFvyehnnO)k`ep2Dn})Q9 z1ww{1TGlmyMw^na=gGCr56D73HJbO6YUofI*S;q54arPoGq>;8XugP$Hu0WY8wS+Z z9jQ>CE$aEzS#I|;Hl$e#%RuHNqdwy+t8Xl#?4EMMw%{CZpNDlaEe@d1s2jsrh* z=MH7`UiVMk3&aGeIngL~P`L5ESv$J4Kq<+YQ++4H_+c~10)KepbSse(L%iZSl$Y-U zUK?~3`kqY0kHv{xBYy{`o&rt?oW+nIEF0Ewjiz(7G13~6huox|IhI>x7G~Qlhf69) z$j=wDoTXv2cxlyl%~$LgWCCb|T_Mebf{u_d2m}*^RCm&~>%dVkTRQuZm(&Zb@KJZ& zc=paZYPTX-0cdj40@}z$HgYMd3xw+NiF*PZl{)>I7~0p4UB)zyc8>NHArbEQb?OO` zC<}z~3)$&hux@AxZ?55GAADB-5wM;$N;gF+r+Uy;t_6yJ1Ug+gNNzE&daB(HTJYA5 z<65)xADWmn6T3Xj1HJmCa_T&nQFC?T0`gNKBtwyE45qDnHfO`GpPP~P?y+iGzt2?n z)!NzuQVZFPf6^)KYZf+ zD-L4!w*N$>)$BD6p>@ydSXQUe4cqh>05+flJ|F~E5FUNS@I_!&{omUmR$*D-ZwcOd zIUEO~naho!$BJBAPSCJ3AUj1g_8=pU<%4$44xnj-k=TU;0)?u4ep>1_8<`A zV522q2+CAOFd72VS~@BJp?A&Sw=rRbP#~-&VKPbK6D}bZHX#*i;S_e^6>{MhI$^hs zArx+*6H1Cw)%pTsM9{wTP+z}x9;mu56 zAP!<5CL$s(A|Em$AwHraHlid#q9aBkB~GG*JYqsjOdM+00hU`Pc4FGK*|hy#%$Xu8 z?i=%^;wq}*{&G7cIuhM^fw;~_RPP6i$SloIlt7ebXeZkDjb9Z?V?{~Z-gOvYLG)7J?KMoR-CI+N(k3D$@3muqn1-#KgtnPm z6|#g_DZwNbVjZ52z!b_)Zlgjh#?0|VT&HSb&xQ zfL49DM}FM@-9^~LwMoH#4TNaU5JWInd=LluM2D)p#~BgF;mL;tY=kDb?522yY-nC%uFe6{cm>!gX$v zK%Ac@M5a?T=W@1^Qbv$! zU1xh}QUrhp`DmsBu$`%Bg; zMFKqt1UhZOkcI?;h9pV;BA_l*#q>mP%7{d~R#uP}TM5^KumSnBf+R$NoNnrQEd-)m zrxpnSD`*07M8|`4m3V;1R;1LKn#)^!YDJ)vct{C3c!h}6f+RRbWfA3yhn7LUyudKrgF+$P{ex{Tc9~cwT!2BDg=op>{#4IfFjA1gvVs4N_%!E zjTWEx%>*k@$ZxpTLO>^Bpk=&H6v#$tf;=V@+GBmLBRJwDLZ}xuqHGpECTX0UHD+Nl z=7zhDtQY}+n;-#2K$6GhMlGr(Vf6$O00C(PCsic@^3YRn8LH$2m(50w(z@JDIL;_R zt+Y+$^GRe~09ayxoO1H)L)Fk-SSh^D<>Y}WVKSvm0NP;6$lQ#Dy>)wYO{_NiXTEacLQ>w+7dxUM!jF#0yN?$yZUBWaQXld{Bk^V$3l0UjjAK`rB<_5Y_147B0tWoW%MK|XwIam8($eZa zRImAv|Lq0uzH9IrZ!7N5C@o8t(ckt82;=@GQWP%1KJNbFZ$gOYY3|^Dn8s4-FaI8} z|N6v^aANbUZnuHeUexaZf~50$m&!&j57A^zlrQcS#4ltcwizV*;UXy69~x#!%|I~E ztg!rwgbKGX3#YIP&u|PEoKg@N4a2Ztc!oo4gm?}Fea@Q<=M53-@DLmSu?^qE1ZOYt zJg{Ea%F+7mKkyrQ%C~8vYfn@a7^+X6Py|AzY)*XL3+JpsFpCxt9qZ17lh$teY$YoJ zZvqoYE=x_b@)RP8nM)2H%*-&Y8+aP=xwSut3cJ=1)YzVPbGL7ha}` za0PM3D5PGfL=_Xu1U%ye0;R_L*~X-^aT{5OU8Yw+bp6vVWQ7wJf@ z4P#N$(n}0!NuXSfP}a1=Zf9ZM8E=f9JvTfKno5%LkC8Zu$Tl_1%l1{ zDz76e1+pPTEa&4L_wfKhG9Q19BnMSmSTW$dN+gt0&-KTPV#IPR2N@^`-nV?-h}$5|X7MkK&Y8Vhm2G z_^5=Ij6ydHfVrSkPpaoA1gtV6%#c~vhkJOO9f`!2%2}8`ua>K-WY=IY@1vA%Qdq4sNbcU`%Z|@*%8X*C*Byx3t zcHQ(=YEbD!TohNniFkNNvubs6=oOHV!(g?-t|de}6p3hPA88n-+is?nDg=cDP)eQN zWEoc9LAY*>H#mD60eJ>^{P(RKhrT=vizsGwG|h6X#tKEVV?nj0r8p+nD?1y{YOpCj zb;wqX!eSi9&>;k(B*9py4Scd4s{HtJ%w*dgctrsJH=Xn+XlZ6l(5ns$i3WSuag;6n ztvFXeHc(nPwed|;f68%joET^V-9f20OK&q>cQntSQvWoactk;X#y^aLy!5oXQBL>r zQpD&q=Fs6Nw8A7T#JNK0YEvn?NJK$wML7$^I3xgk4@8myfwpL7YW(?lQUqxe{_pmqN#1QySJ9T6M#|>848THT>wmlC-BP}m3l1Ph3Cq655SO9O=w7RZS_!wc* zLx?j)tjD+iM>`w@`%vkeP{g8RNT4~8c)YxM|8E5u3o&Q?T06gLZM+xVI| zZAT0sMNfEg0Kr9Mu|xQEQKl~RjKy5A0$2aI@La~4^aY%a`IVvyjcU^P0`I5B(8B_~ zctk;lXl7MUIYT^oB#6DHLO!J!9!`+|?hXxi8rJl7L_s7_9o4~EZeiV5oCP3V1jPyc ztauTLDc{6VcS4jtM3A+Z+sH*sGXr)-S2%`!Eowznitg_O9YRlSrz&#**4L0i48;+1 zN!vJF(0V57U628yY{Vvrka&oC-_6Q<%sN=0X^%++Hs=#pF$8gZm8ZYSF2|gZsXo|X zC<54i-&YW#=yTH-NN1S_btwcuBoQ!BfWUzU6G$SsFv>tJQWi#(NH76IfJCTAP1Th5Jh211X8qY)HzU!Oo9k@29?NCqfnzqktS8T zlxb6^PoYW#z!Hc@pb?4=1j>^C;y{f+U=lP4U|<3yNPtGvm;^{#l0qYF8Z~gFfVLE~ zE^J#@0M~_nk-*H#muf|iYPHf8kk_xy1S7d3s0#q-;fQWMHeTp9YG==%L5CJS`siTN zr%{8>L~wOWi5U3`&=fIB*v&t^UY%KUpaj=3BSOq+@v*~=kq9CWF0p1#Exb2sRK9cg zLBG=lR4=>Ha%%72!G{+=erH~tv6rG3Dmk@!@}=Lqe?Omoeeh(v+qJ9Net)O(o#MNCPtrYr=)hlLCRfv@lJ> zkaoK=HuLH-56eRlRVqpIzFJH_LnEr|#EB&RQa+g)%`vu0bLNblDKfFtPE=C z*PJrsX*K~s3e&b16FMo_B#=$2p)Ho3v?+6GO_x@#BI{~Dy4v0DMx<~oY)*Oma@WRL z<*j!jc8_vT*ht;~0vKQZ5dPQQr1%}J%L2yNC{u%?cvVVA zGjz`=Hu*SJR4Z=T<(FZOS>~B(7R|=|2&*}~4UvF=*DMAC8d$~d6)VJit(;fi+m?#6 zXoC3NNIiZfbz06Z6*dZ8hOO4w>+z7(?8JmS+qy_aNq#D7sFEz&V4>RnTJCfoi((uB z*a^z)xkom;QZ$F|7}T^sO_$TLJ1!=@Rd=+C~>|i2aVOj4_}f&KO;p-As{Dm82hZFzY+Sk&vxGY{xo{>>GRod-~IRDk6-@z z=@%+>DekQQKY$ZhJ1G9V_rB~g2?!?A-vO7Wy#5Rjd=v5ClE}9{1WwR1$O;z)&xNW| z4R3VFbDYj5_c#fXW^d=gT+%{_!Iz0ngpS%t2|*LL48o^{DU1%}PPi1#*-(WDOkJEF z*u&`+P5unW7bCm%O1wk&L4n z1(nK}Mm4UHjcs(}8(r2I2G&o8FoWU?kK`&MHV%$5t78#oMwOV&@s52wU!hoZtw-@u zg&S1=Tn-J_Iu@d>RgyC#ijFE%Yjv=qwpf;h|fbZw5goS#hcXG~>Qu9(Yo<};x=L5RpiLBX>n z0z(;1EbUQ&+Jv7VVb;t;iPM|!Qy)ie=*AR6Dw~jOlIR>sx=NmqapgQzCYiE1b(IjG z4%XBQ8#=P4?UT%4?m)0{RHG)9FbM3tshv2L<-YRn7JgqEvLT1`Fi zykVnwI6D{4wLDmhBrfSXI!(?qu4A1~Kg*U^ydtWOef8^MJ?h3=0S%}?g_`x0=N4gI z@U9#rVp?44N^aIjM@sP*mY9dwCXq8LpA8L2b&^WE>Sahr6zyvPTgKL;HE1$=lL@=HHSvAG>J`jDRCcZU+WqFr7){<+geYP`^Mj~61DGhEF=T~v`pwKd}zUu zZAMa=u7E@!5y0PF1d?HjCA=d#p2|QKqxnXRo0X03c&ZEk8lXfW zTDaK?X;KK?7YT%%M*Z(I<&y-?#CCom>|HW%OORyF1R{+=ma{@4i4E_zGb5g1q6Wo5 z!#;RbG3s4Cacd?021}8T)S+Hy`kDxY={K=b3On`FTo$*lDgh#+V+m5@?Qw;&JmyLC zs#B2>M)sFd^ZvZ|diQVd z_j!ob7_ricU9y?Hbf-!v1?Yam8d=jV;t?TV_k}}pnbpMdq7qXh)1wC1xN_C?>TPXk z+)J4FtaUVl92`_1lhqf-n>7^L3C)-1ydXqd8K%XasAlNaI6Y}-Z0Yt6pK1;icg#=m z!j|EnK2o;g$X6oml}hzhu9SUXmPzvWMbGI-IOi0l{WtO<8d`hSv2&8HE8KLH4SXdi zhfigi71ypbYrTVxN_`d3%#CjlkL`=Zu^#yvZ-dGU%sx3K|34ZqNd;JKbeN>Vlyl^??wX*0ewHuUz# zx74aLO>w>3A>sheLJ$qF1b=L`dSlARvp2AY%EmW%#P%-oIOd)b8i5U;mt&(|A8sN> zLWRh14IP;t6{@rEWUYAov$dgzIEQZH@C-k20nUhllL+8skRf3yH}o>8R8kyaJo#iI_$pzH@BQ7B zTuDbB+b0Cv?CDu>6@zI7RnBN~60CdEO_Fc?70Xl}$3i>NO-orc_OH~b@BY!!^(OUT z!B!(#V|5LzJJ1hm|1#Myndzp0J+lo?=(nK})}Bx1*TZJ{7a?u*DXsv`jHj53 zb+p~LTcTB~tDVd!FU=a+n6#a) z&&e+F9I%BWeSHGlQrSD;vxlA$e_#VnDB#Z!nkjGHY*n zjmp|6BkTTtj5>&fm~-hoF9rPSi#nw^cOI#@Nva1{C0i$<^AvwPGgAf_sgr5{=uzX7 zwBesba{kw91*X~VQMLwmCot+p3mDH5eeQ!Ik2YP|a-fMkvRKDDs7@Y7Vhe=BN)P8F zj9^ISS%fY+0!8{>V3wR+=l1G3`wF(504|m^-Zhm{&z{}7PD6KVLrC*(F|lKjX_ZWg zcKXUGeI6vk-i{aM%wZXcwoj}#H9&E+>UEYB(Dz)AvzpqVsx9%Sb^G5buddn)lf zKUxi5<|r^c;kD%KSUcv${EU-TbKkA;8I74ku>dc#y6$rp7pL@?c3(DppM*{tWQGPh z1LVSjK(OcQvQJ)c%?fZY&b`e91~fNj%6mnc&K4xwaJYD-uXu6qrl72J6CP1&KQDEg zW%mj~xsX;Ty!+ni7o$m6oO}f~Y=8DX(&?q^9&)BQGB^$6%^xu2Kwq$>{lO~Z+Dhde zJrVqRkWVw4qk+{mq-zh_S7P+>vft%n_EFOKE#mcC(zqA8*9RbSQow_Nz?BloR?e^( z!T%1y8Ie#QK@P>=S-JDqGq8Ure3*}M2{?(Zr@OGK6xjXa4R9`jUu zX0y0cWhbAvJnf&7nKX(-_L=Z`b23Zdw(Q^TcSkSG))Y&`2n%S5iv;_I&5*AQ8oF0s2Qg0*J)2ZAn^VPc^heM;-N zI?N~uyKSXju9uroZXGO>dz*u?K4_o&#)2nf2cFWrff47`IUfXHHbY|{513#uLleV7 z0|8XAZdpBdq<_yV6c5%9?GkR>m5zo2uKroXvTm7ll;>y-%C!UGx zQDX)zWWrAca9CLb5_nl>wd!}rau*;N{yyT%d2?7mZwO&JHdbFM<8x7Ih5lxq-@aQq zK$E{J&S~;!lFmzti=@4v<^{>k3+{+MkMdTZ7hvgn`ECZCG+kF?as?Tr5p4lE=C61R z6FR+#r$aXn~~&01gp(n;;ir(p_IY-UwE}U|=5&97uB>GsG2?T zw-eG)78v%*^;-3(P&Qagv2eqOskXcHY-#C6D;yg-Ur&fUA8RztukUAnDwqTDA^)Hd z04wCxWl7+#BQ3HtF9stMx7@NFV^xh+E`^Tey;X#v<}So*52vS+yPvyH2m<6^uNasF zGYk9izU;%1*Pk(cS(^1o&s+5Y|5|=u$b0Qts6)Cx92_*y4Gdx&k$YPkD54m^XnDhY zfi?MbOGmde0fTa*u-@Ty$ZDYKc#`TeJOmhhTNDwYCd)5h#Pf{ZoMRkR-BstZ?1Wzm z!GK)Mgi-A?k%XA9mOtuqjcBF*{O$6^r1@^729)+!jntsJf!|i3-v3NTG^gAj?loCf zSLk2c*rhh+lM+7OUN9{jZRg&BBPIHQYJ$EaYzxhgI=J zSu#02Vc4?RYZM!st;FU!Gd;kfTh#D`p9LElEQ0|`I% zi9{^U=~uJ%J#wbTTu2KK>S`Q#NIZiDuBDBojJVzVUe)0)lH`D1v)jp!rkmjSq`9%_ zeaU(cK;ZZKsSK$E$&Hk$h5dnX=`raAkL~XhcAOdui^Ak5rAmY!moy_~4W=-qX9Uqa zR;RhWF_r|BgC^zB5N%u{TluG2W%Kvx>5JwraBDoYh|YW_ZCuKb`wC<>`dJJw4aI7; z{mQ5h3JS$lV&U4(pZ`p`bf-Z8Cbj<+tVnb3?FT7)aXERDJJdxZ@k+WA9Gu+y8X{DC z4##0m#x{LSurP~}UMt5#>D!x5-{tyj zON*_p=krV<+w7JOj&03@oxPAXVJ+???K?;t(dSXs z!qXgfFeJx#ePUjW1MA(z-H#I$#N}6F;AzfPZP?n0D$=vw5e7TjUV zKaERq2q+X8NkZkq0c2oB2u6`y3>KJJ5$07ilt?ETbPo_H8A*Yv=SszaO5|hN%{qf0 z@de8?((;-UK*@^VVcb}4j&|(dS>>YSaT|dU##C8dtbin@Hm@{{N~|y)LJl1@tvC|Z zn@{N9tQ`p190od5K8IeI2{ka;+l#3W;HtDl4`^mw$qBx@{YM~H|GgJdx?W;RGNKZz z3aL*>HDWZ2A!a6N=Fs8Zn#`Ac^AJqkd@x(0c?&LVM=Nvd=s7S5E6ZXP0ZxJST%Hr{ zDLN?>cBO$uCYY`2eS>#8p(O*O>K7C683A9_;k?Z5B75* zpM!edEO))*)g$Q3Hyxl|S42x`g&Tb(yQEJKp-2&U5+{D3Oi}J9Y93^*Nx*qD^1^2{ zPEo#XX<1ULe9NX|=iF;R?|haBw~1q=!O62kDpBZK^hQxp%1M2I+a`)^#xgrGa^(Txnj{#$MXPP3;r>ee#jdHI zEBP33&sdCjY}F#%T=6MYhYzqsc_gdBSmsPR!P{q)^1}^OxCsOqP7Wv}&loJ|%Ah<> zK@O#qtc!g<0^(LHY%i3xOzkCA7o2O;_sVo$(9!aCwj)ij1eScO#A0%usghw`ajflR z1s)|>1=&?Nr!t|bK~idQ)v?B>W3Ek8GRG+qSWGQiSnvicv?9Tj$fHh*RoH5!EOFQv ziJ}gEq_H8mVF2m;lfu^&g9`W+;RT1*uOpq^FRSdGTMoa;2GI!B7tAmu)CVobwJpa_ zYQmEZN5%8)?WZmjpBOqa zX$IKB$g7pbvYJnZ7F|JLw+Y&Q@2^ox)v5qv)c(^U8yx+*%0%#_Am#B^GYzr&Bo+mCV*W^Jy}R#Z_n{g|4EVn$1kFW!rl|K+jMv z*CK-d0R2=+(5N=*DK$ z8$GYsX@yWqaJdH!hmTH0R0Nq#dP%=v_Tz>S~gQ0 zwGYC>e$ag#-&Fq>=#X~#EX4i3+V2nYa}z~ zT?UvCS7m!_wmSCDH`TuOIWZff1o1~O{&mUM8C8Q<$-@fkGIc`6y5auN_bi9Uw*jD% zR4DJ+;C~aBs_NA>%ds}taYZ2pO%G!t8h6}hDz)2G@!1r0Fi4VYa^6bVS;=PwI|Ck~ zd`*Wd-#9oRH!Jmkq<^cnt<0czqUs88c#LNG<|d5;a;HqS!$@X z7k?Fj{z~Pb$oj7Px&D#h3(9MoA zj<1Sho`o6mre#*`r4Xre6?2Gs@MFo@4ti;c!nrmSv4TwW$Q-x^uT}&HG@9!$d~7nB zcX6gTeH(IL5FKh}?gy#1zj$23>uc%ai!$eu3uQHwdoL1DqSv@Rqt{lk7Vc_8SR5L! zE$c^f7$)*+4ho3;44~9W;bhA@8`Tt|J-lv723KP4Y(w^03V@Lzt_OLem`#KS4c#R*Y= z6-nfONS8thGD}xA&a$)emJ?wqp^PY6)cz3&>^mS!e{+0TaZP&u z(LtFj=g$UPbCO5 zJ9MA=XGZk49yo`ueZ9V1N%QXCN!C^)E46%PD-J%h%GAZBuEgrE2YkUvVw?klQIMHG7vS~zM@yBuHT3O zF9Pt}Laa1gwLQ?iFASrCA#gA9eFV;cZj?!Yocoqp-=NulRAGrMk>>WGTO1QpM?@4W z1`XxA-%1*GvC%;Wb{9sVKCm^99E(My1#C9bE1Q9Y37^f!K6=R30T3OVB^!D0$`)>T zkftMS40;dKmszCr+K*xDi)NFfj0Tvio`?Ge#1+AikvsgVvP}L@5au)tSyJ3NOzGuF zofg(d3q28Ji?J5h+<(&%WRZdz&xPTe%khDJ)`qy@t&R!nioyOVIA$1>T-(mbWwLkP zF|jCIafA--?Eb;T4E$7>i+m37=A)6j7hJagg*2E-Z*8`n`<0 z)tb|@S8$dp>4wbY-rDmFkc@dFX4fOk?8Py-9p@p(WaVvqr^}aXMN7AANO%>8<&;8M z3Ht#D#kNwAdLnO{r1%8v{JdylA>R3pab$~0RHLapIBzepVw)(0>)1%f&r@>vN#DXv zIagDq@n43=DifvI$tL6MrimG~B3DY3CB(^|4bp3M(Cbvv>*mnw$I%-G(i@x5n5sU~ zm{aS{3uRcDWY`2|*ma1Or_*Hi+Bp?Vv0W;iG({p7!+lmGyiKxv#e<$4YyqorPojii z6OYhJgn!4Yu)wUCj;z?5ETmI*++lY7O?D!FP8xeohIn>LU`~2YPNtJZ$Y{>Tj+~;^ zoN)2n1oqrQlici_+}w`b0{j;iIPX#s^uNf%>#F9UsSYKK{lpxS{^|m%8Bcu;3oQ@b2a#_SnaN9gqBo zgEV|}VIY||!BOFJpxW#oox>-cB3kE|f|VkQqayHa5giALCAWxD4aG)- z;*jv*z76E*w6KB}uL&2AkQNIei$zR}b-jxva*L%pi}%S&_#{erolB&FB`-Nj)YM8e zf=ZN-CF)1TdbuU~9Hj=xQX_P!rgN!zZmDHwsrFF`0$BEnqs$_x?7!SHYY8fQs_2zf zk?*Wwq0s?;YUTb%Wv_F~18CCU28DUtmIob`XEl^Z9YsmES_?Q{MVyidy9lzl!jn%7Wa=0_Vznnu=l?bfq&oa;>tk6P8zQ}tyu@wZm!iVbJT9B)$Rx7elx8-M%Ip?YkzXo9MXg;3ZecuqiCG#IDn|T zT-0??9W1x*0bTbGSpV3G!iulQm8_>Pug8B=Pejo0oU`Gcrk+&20q;!%wpjz2dOclN z1NCt|qe}xlXCp&=12n$zMP4IYRU`L!Biq-^J|u$_dy}wBlW2SsX2yT4Lh3SYP3?0{ z9G6Y(IZa9~&7r;Nd|`!=Ta7e#jamdPOkIr#$wp1i7WFsHCdZG|LRFl(N9ZrN=x&bI z|0G+ROj|8oTJ5S@T&iHMqpcnUZGQ4KYYpWOznk9nws=*wvEfAt)ado*l6T3wnWFOMh>$+AGnWVfQZw)35z<9&Uz(*5*pVMlb&l*8~4lou?~ zM46r4$DfW{cv@S%up8*MajDH18HlhrDSDl0O*_<6;s)DD_QXK880pjT-4%aAMoSsX z8_3YdR_Eh&3|^Geu}s2YTsDFsZ@ewAW_a%I-XC)I7z;lNKg{yFeWf}HsfBLUcg(L_I zCor5g?>mW^Cqu3d{pRbJURe;V?{80+`dY@n$Pf-Xnd>ox3#d+*y~Sbm?-hfQvT49c z4vZ)>G40POyYi}voC!zdSQN$eUcQf)NWT;(XogFsw~OPx6CYxGkE58H|EiH@B+E4z zM*hk$e)I#OCHz29Q8JRmn6#B$eN3{?S8eRY?1z$1k7GnMW1|l8vGE04hhuI&^8@3(Pl42qvz>~D>tvsh#|yHBKl!o*o_1rN+MM=FP%6q zM6{5H!U+qVR@XHALm3ht#=^`@qH95-uSsIa{U|^Xwy0OLXrM`gEW{B^07c&~K#DRP z+%mVTmK?d`)3G{ajF!BJmVG`iIZZBkSS-77F9#+r`~O^e_jx&le&x01N|5Gq_}i7J zjb$X!N}StD@Wx8A^ve6hmDHar@tUg%(yQ5%t0@+%mWNzI(hE5JYjSRDl5f|f-AGCk z*J|h&;$~V_5Mj`iHVwD0_Xt;nW0&l;0~GYCOWGD)jC4_2XXE0joE?!#73|~Ysv%o{ zZORhgv@V9b-tM;ECvUTrwlOj({q^m}+Q!C~8{Z~9>2~7A9?|BH#3zB-=FhjAryn+d zaeq74{B|P!?aJcYx5RIEHQ)YOY@SVidwl!&?Ll)Bw|f&O6Rv6&S4QBnU9Wz=CC zx7e*Sb+zWMsHL@62UwqLNcsosXG$99W=KYFmu`Es=7V7QrEYmT&$R&OAU36bgWjd2 z4kVZH5h(qa1|gK@CYPvK9>n4<1nUqr)TVctg1Op}4V#kCI_H8t=E^%T;TBUjaMxZq@2RxTHLIlY8nxDGJ?i(u6rd_&{q z9FyvK3xi6x;cC&Hd62%r^YRc_dNh1n?JWLCt4s2kwBZ0!(EX1LH+MGlU%QuV{w2dNMGKk;|olH7CqP6T-y)mve9etJUfg zZSUnkGg{kOS!2e*RJWF#vz80PT|SQ1qrmP!pcNzeh0;Ut=fxYJgV|E;a?6cdzmwG# zkC&Z;jdEvLJq(6Kk3L@JQ4-JnL#X6RdyuL!=uc*d?Tf+`ZVrkb4(s2Ss0!XxGH>SO*#AFuEOx<@?3>60_+Wk1me^)&WAFxi%{$7!4xiW#AZceV>!mggzu`|0cEU2xPiQ|TTG$F^3a zmMGeCsK-C{kl5Cv_F#RW;mDOVlnggj0&2DhTy;^#?4kjeqj$MOqo$%YVTR3y@{Ggb zIv_^=Jae~=e!OZcq&d(r>Dkh?#Bzv*Q8 zfWFsoX#&*4$qWOmqiJ7XTgU!OB(fIa4EHZ>Jj-ZdOnfijvP`Kkr&rOK??&Mf3+is9^!W1&C4^*LARzfda5Gz7|^E)h9PDZ_Wx0E zxIcbu8VsoXJS=kWIY4GaCqe&yAjoTkIV;$EjI-_22pyBgwpvmr-u*@5Bg2u;jJoJQ z--WO%C*IM-KDsD-*9ceK!|yzdbJc7<{>f2gE-i3d;A-ufanxyZ%V3`B$=8 z@WF}c?X=cAG!bs7#`(9hIe%Fk)-&q?U3Elr3u(0CmXUE#)_0x~Q9rjGQ)v9EvQ!1h z!CPL~ucKJC*JTYZPW3^Yvj~-==(c|~>cLWGpw+8#fwaU4`!HB z!(pID4pep@wT2TMR7kZ<`LYCN&ulvrNlqG(V!VaWE=ysicgZTLIhjh7IZOr9-@xG% zsyrlW1WLw5_+Ma(%u~09-bvf39IH>zkxG2|y*5=j=f-e;^Qqh;JJEASF0BHxxYPiF zmzkvA5U&n(mJiP;7{~Zn-AY#DO zMO(s?+ir;S|1(F%g8F3fNqw|%!(u6p^wKz(G2!! ze~%~;_{zn~GtH$a`#D*eWv7U`1?lCByfSZ>VjVG;M<5b+@RUe!bo4P)KeMo@g2xP-5kcamuCKViR_s|d zK(GwpHCF=#6+0Go>IYrN;pm53Q(0jht^5{1 z!!>pmF*#IANXb6Sc;Ts{y;PD}>habsH#Evd{ga|o<`SdnQja@7o6zcR57)wYS0pG9 zLBj51zFazi|GjRLbEF2+1muU!iwJLIN*STFxZ817)h)SS;CID`-q_ zM$%6a?3zI3K00HuAPl_YO!)<(HXuzrK&AxCeuw{{Y~rcL7t|3usLD3(Nff?`UNqx^ z@`a_c&N*j%n^5(T1Fk@^l-#dKwc^Z@p)sbdnplY3Dqd3q^6T>%6C3#_hI;7ht z?Z@8NPG~iI+n+Oc@Jc=jZs;p!!c?s8P`-_v5oHTRcBBOx0 z`t}0&T_PhV;QE$kc1wJ!UX)?kC9HVAxjgCQK~8RgA?@HI3^B~1O><3*&$Fy@9N1P- zBm_mR>=#4!IK>&T3g+Znbl1H&Bk{Kh4@fVFI=`eCzsimAwZSIzGdSlq!G=sUC1rwy z09g9?($_+=Gt{)Mg|pee>GT$C1*0xzaw$AJuN+sc7hD9Od`G=UN+ ztjW32*)KQ9=U2z{eROKHt*K`#_x06FIvbIGUYS&^R7?Q)6=lGXJ~*Pp5l^KUMN`$ish$x*?n2*C@c~ zT&l43_p?p`GzGAhJO0%a(I8^S;cAdaIFoe7{`B7IrfPrayf7X1r0 ztF;m)3Bc_*#d#A|Ge=)nST8eH`}8?JeNJ^@XgAHa)R7rO_YDwU2(lMsep6(hdd5w4_CV3eZW)ejlpy5t`ktYOwHmFOeY`sn#i!o#^ zrEI4$wDMGg>H9RZn)J~*n@0o4=BjjzPx&xX#mUHUd4=-9DBWs>HQ|#8L zWfyy8q*{@=T6J*$R{$@*5MwB3_K9&X>3vFuIkM3g*zie_dqS->z5hWzr?pZIbvBB2 zr0q-}uPjurXdkbvS68MRYbDeWwrd(B)99Dd7}z2Hbxoc~I6U7w{>~(|96VYzF&e6& zG0`kq&D2a*-AkE3r40q@K&eE{0na7W$LCsdirb=zb*tky43s|Jd>YFfP|0E<*R?hU?c)h^%UuBrZD`$8UE5l z^EPd&j%!w;Ne7@gn_XSPDuBAFR+>wbI55-E&#A7BFUtl(YVc#KyFE!$^pHrM`YA8SvJywguFC z;y<;Ec*teMQsQot!;2Hb<-I={<1<|e$_Ne?FQ{YnRdsP=>#n7?5wgtg%ctbZYjSDo zuO3qof1S*I?4(-gl&}seW9FQG;G;6`du~NT+U~rNUBSn+0*Rb9 zBS8WIwD7OM_Vj}c(=-g<@%c5F^m`pnck$-8-c4zZu~8hH{In6WqzS#LSe>y6`x6EM z7X3f0a32p4T0ul9v|OqNfN=F#-FS)iAVF&g?hUfFr4J?xEqg*hZnOg^T)0Q5M8f_o z%Q@+z2pXG$NdHx&z<<9SULz^sOS@0V>$pIM4}yB*jWP`SpK~qr++=%Mafc`oZ-#vy z4y_n`#94XoR>(fi?RiOzhmTR2NAIQy5cv+6!yrI9M=2JOVd`KsRizc0cGeqL2X;wZ zTxXbZ0yxo4W?pQmJpe(PXW+oPBMA0q8fL!1m7eXPrYa(0Y=TR3lTM-`tDpG6DJoDt zwr5@)00apsf84ZQbqtV&a_c>RMF#9ZnMDE_7OELlm5AA}dVWG4y%OHIg7un!Z@%!o z6jK!U8Sy9e*jFDao(Ma=V-%@p#N{RgaRG_VZ?x87W}PuZ8)xz7Yxt9U3pU9o zAXE-qX-~#Vpvuma1i`HN!ezJflH1$H=%FwRMHXwmBvjIkQAW1vyplEn=`vHT{_F7ouxCS0 zfuyLIdA>?ytDcG4_(MhUNu~&(>Fq~+(x59cNYBPPT~%bc2xa_OIH~TwdmBFD>HU&n zEw&{<&D8Qc>mOqQ@QkLZ!IT0+n*0~dp9wa3Umov7K^H~uWw`|LMk+6NO{3s<_Yzbz z^Zb5Av7c0oHtpY3$)czb+WHha;QhCXazHM9N}OI09r2d_K6hu%l+=d9&1cd?hjZxe zm+>9mJ^K)mf;V!tzzoM4d;ZZv+GV6&O#c7Vs7=+L(5)xHzfy-E*|TGL*zbg zB2w+t;XN?}A}KqW^<(-!$F$APq@|>&Zdtx66^0T6{`>JaTD_8qj^23`))_jjJCaTx zqx*TuhZlriEeJbI56l!!6%gG!1rIs=47R@X>?yFE|M<~aLmAQ5F!6YzeIJa7cb~%k z>3nqU{J}{p&Q}L5dS@Oa`ca?^6D{ z_!H03!l&a-8P}o)qi6>58q1b8JeT_f$5E}rG4qp=2j{#^$AL}PdVBx|(+v(T_W$MU z#=^kIxWnfCpT2G|>{VfJ1b~dge5AgpKZb<)|A((z%;#u*w5fc$KsEwMuh#s(eBG7v zs%DTzh^ng z@2m6;`juxLl+|{g*z}$=D{jZm<-@3YD(Q;Up?l{P%nl=GR^sl?Mp-UzROH9Fufi~5 zv|2`fD9g}98rdj>_~ohQv&7LTGe}haNY^)X+LcwZtlZ5se|=c0D=Prr%X*urx|icv zmZO*A%DPaN>p8@}pAX9&)k(+SQr-WE1s~ln2*u+#D2$|VuF!nVfIdJah#VPYx+|km zB}sf zsV#V`uK%UR#{QRfDTb>%4)logeAtW~a53^hSr0);W~Z-UM9lBT9vfm?mixAs>0L(zh|3IW8(uO%;YiO*{iRB8F< zA9aaAo#LN&U=JLlH4|a3*Q=Hvh(t#`Z1w-H2kh7U-3-Ql2FBdaOcIz-Pr)8$Nl*T+xrtoBhFvTdcW@R;r{Qh$43mpLX2nd z9xR%(P=H1u7Uf6}p2S%gNkSnG8@v~2dKNwdp~Vwzub%Kai(n)y0;s^{OIK?nVH!n1 zf`EGR&a)`Kge}A;wtkwmvuKJ)I%5BJIwEvrv=kwVsiSsy} z1QfU&KFD#T7GpSpqG%f#Dw<0X28io%Eo~e$=hu{P9F;PsEqa(@^7b(dJI}JmTN{JZSso4`HtWN?9 zt)PpvLc&s*it3ndu0T+RR4Iqi=$K*WMMix>DVKxlxarzOX8S}bkN@bnIN-~GKW$L071MF1mnf=9*a;Mk;L}aWp(m<*D%{RF3&#|*XjZBjjm>3tUP`S6v(w6P z%>Nzr;Y?i#A9XfW(uGHt+uc*>#Hi^z2{%{hE3)fmjpD!u(;hVRkF!)i;7BGLKwnpLLDV@s`_e`-7vkJIgKbviNy2(f#sEdGuycTbVk zgs?EtVyUn6U1>Q)YgXG(j;{oht^0{)v(outO}a*bV%7wNX0EsdFN6T3X+>HU_( zP1(a5djgUq6ZTp@A@dXVyc^ArlhIu}4k5sOxmSM#w`zXx&s0174A>uf%OhBbt?4*C z(=n4yD}+wL(1I?4 zIoCG_EJEei?XCVb=*?;5gwHi-qiC0tRLIZ-mQ#iwKir=p4dy8>2!o# zp)Y^iM>LzRt%TUczChtV$UxUt4Gj{EUhjM6y(#)nvaORqyhN(}O-8om-y&yG%|D}e zo7Yoq-G3*4g*+)qbt^2vJ^y}%)82mvXa~?@l6gdxS8Nf#Zy#XOI!Bt_Z&OXT4~b5l zCyDN@F+A@8#&KU5jdtw79;YE*dQ%r+be+2mz);zx{JxCG`3=trfe9x~pX|`!{qaL_ z_!yDzWp%a_m?>a(RPmxT{%BXnBz5k?L%MEViKk(Cr#5PwGS=m@y2)eE*MTY6%P0#L z2evnQA>E+5s(+py@4MFbwf=T~{dXEL-L?65>Tmbmzh9xxKYi!5@yL7Lt*iO+(>CSw z_0TuZ;H3A#7wbGXmYSb0^sJ*sWW#GA%!>OjF{Ssyy!ndE2EPddOHL)d>MER+ zc{YOcrs;pDf&xD`d3hhN`?Viz-Cm@o`+MCiPlvpm6JPxKi{L}zH0kZ|uO65bOAf z>x3Dq#5u9V1>?k}fW(#T#I^Rs^_9fU>%?az33Dp(iSkK(u%s#bBq`DaTm$d3a<1}~ z(35PCeL!e(TJ-sFbZvOj=b`sJUfSx>)PGjMbuf>k3NUY4q!Iu7cQL^{UdeQA$mc0Y z(ca|weY@{4Hj;KajqgwS#@&l=y0jtc6<{*lr43& z-RF4dKFEJ>UN)HE#ce?Q`Hzt_&i|%9bvzwt#p(rGpF95?uQa)QoBG`K_t*FS_{SI8 z|AW1^42yE_yM_lCI%J0K8af0NL@b)2Q$SFpyO9(TkQh3pySqCiq(fR#xv**3Z?bqggsJ)pMU|(o& zX7+*~TjknA@m|=&I)8`}PASV!Z3=R$=-h zOPZH@zvZ&|alH3pS(T^sukXUimRY_lrnP=`=v1&#>%rvlgmGeB_ekEr$ z(dA(MAGCKZ1;+4&_O7Lch_S4vM_kZe6hFOhw3lTgJ3Z25BPToaH`*(Eww3|#Pq;3Y z|Bd#tK5LLUep~$Xm84mI8^u3pFYCMVk>6=({SjqV^ZijZ!-D3}p7utJx_U>@j4VB|)&XdUEXdcE`cusq_juu1CX)o~@FJw8| z5KepJ9k$@K*N5ZePiXJyX8D)}oc693o^I7&CbECugesRD(_(o4oc0!d-0d&yx}d$E zK8&bZe%imFz0(fwKON5bT>gBt6lMAOcrB~w8||fPmHc(ut3aEjC&Q`KUYm?^t+V;s zaoRJV1t1#T^JY-&_66-#?Q9{upuH(rhR_Sz%h#KNvnp0eak4E+|8d^!%$4zi_9ll$ z3&4C@RWfpXJ#}dDr6BpfOeKoGAX(zIOiyO#9uLhQX|HN|5YIX74M-zjg?8{y?gTmZ zrcoZA)83t6H>`B(bJ|O~8v>I~ryX9ce|^`Q0i>1=K_&E+@RLG}gwtNeYmG7&w3mLx zgdR?NcO$Z;e~I??>X7b5cbcgLhjsND@Z$xgI%Zxi8|^Yy--{jBlc6hHPcXOHi>r60 z3n*@OO$)HLOc!({BqAcSO z<8{kpJSa&CzriSC7~RGqb547+#jbS^204F7qy9pB!(u6OWn}@2pqnc5543kE_Mkjlroe;-c2A@GQAL>(yEqhmz^loo zvI1${STI^0#Js|j7gLif!@(Gmi_sKUOb@e0sJrhqr;<=hk#89er@iWjwc~vjPo_tv z->TS^O%k9vE@Mn=^Jmws$P_t`^~@ZAMRK>y%^pd3%2{%JCkAY-aLV=T`TXak{}1Rp0T;=@~=Y;c&_CD;Qs)Cx4#ZG0CjP) zfCmaTt#q}#cFVI%|Adwkf=0XIQhi0cKkgT_oLp>7Z?7Y(%_f*ZBYdb;7ED!HEp{ic zMkPVb+#eO9QNX1Yo%Ml4swt^mUyjgntD}c0 zf)Qo1V}RBrOksia_te4y7=#EPh}=A2S`xt|Y^C)UuDmL0$6p9c6O?0EkHS|}VFm;W zJR#H(G4IvGye?J)yC2R$bBxPM@16sSRjFKO5YxNu1%FK`GP6P`JF_6c^X!RxBtmQp z$14vNa84rc^|u|XxciavkHDOH)Wf3*2G`dz3PhEeBwSo8qSNIvywXJC>l&~g;qCEpg9Hv7F*N!nt8BxDt4s29ggY`iVelTlUC?9>r4Gc4oE+f;Qd zVKg@w9=^Rp)*%Vz+ty^l8$Hm#`)V&8)~Gwz2`$s zh9J0LgXMgv$wc%^{x;N<8q`CQmCuJ7*jF%g@V;ri{@oBvFf?e9c4%3#X#Y~@#lihS`qicQlm$Yvmt@smEn=XY~ z8-*6S646u5>Z^|!3*{*-9k0-9_i=lNm!(SxWk`8C>leJ-Ivq@9ZF?wMI;MO>ceP|a zdhX>J}1zj+`a#+&(NVq2hHYA|hy9 z-HFRL{X{Sy9PwFUC#mZ6%8q6?wLSZKdaiLlQZiymSRJx++fy86b)>^Efn8-z58~Ed z&+;s_6HFrp2$4^T{g7AtBrF(>e1IZpW{>#qx z2sKGt^YfuLB#c0A1s`g3c>X{Ue-{P#P+Jmj&#b?S0Uv79Q2eXcnnn1*EQ@?I@0s>H zB@LE?^~h#v`&8@q?(K%7UFm+@O>|9?)Y~9gnqKjvGW2dOIMgdviS8j>%ZV%quFPLX zvF++1C@hI;%BS~t-tEyMRWh!-naNET-K)>P7gFt#c{NL-&*XflMGUc(b@f>|@5Rkw zWt|T-8;P?n$#kar@~t1_JozK^-nwLqh#PeK zs*mV`8nVTPg+&-djHoS_X>-Z>eyBNg4=0|KKfZ&V zEAKfJ6brINnoa!t=ch}lpD66 z&x=l}KE^^?Qn-|E7Wh`Lmu0Nh6~fkS`$fyC?a6kacs8gFs-iiW>TBHc`pT9M_T zgfYcbG!^|q%NY%2GZ@2A3o3t9mMkiLuH_6ZWR_*hyn~izOunu?s3;CBFf){znmpHX zs{0Bo9D1f^P7dH&PN9{L)bzp(VNknFp-oXOOP{xAUXR@dZ`zdlvS3tgy%(#!UEYJ- zM-FwA2^`>2cp9TKy5k2?b9HYF%|pT?)cPe# z!i78Mm{;w69Ij(&7kunQCCf-VdjgHF+3Y^m^lCE7yd1-Rubsc}_^i)rW-S_&mw?I7PbMqce}Mk+J+D0uva z(*3CALd%J7-+ytT<@icKnq^1~Iw;7)aDw$`=+2%_K$YsnA`Xw`m=9kQ8OH1kLvOC! za_-OESlfc7PnBi1)C_p}dnRHoNtitETI3Fd_w9T#!XAV6-2!{=Pm{+Vy)p*K`aIlFN*FL>xUb->o zGi;xA|H)HgX-9+YNZz|s4-9muT&yt9WJ0cp^x*R^EZ%Alx;A?c;3M9*x~>{F5X`m< z->55%vZ+Uy&4flox#Xu7YY(GQ4irT_(HElv4V!u&7V7Q}ySnO*>UJD~_n=#L{U1{- zYL1_OtXGUl)I}6NI~l*mwr|b$X)|x=L|pXKS+~ynC!9lh+RF|Z?}s0@Etj2nZauvt zRmggFQpEQ8Kz#n)p2XS5(^o z{p{xWv)f0{g1nyztO=s6ISZrR^0D#hVDeK zeWiF{exkmKL=7pw^SYU3FJ>T z@8j>0Q$@gYnl1s*XZ8kwV$#Lun@%ipoM5Yr>9NQP_^d+?NA5Xu>(s`IsH`{LTc! z5tPE$v%+pVF~2zsXQz86bT7hgIb4jH6*oOx!k3LP{gy+#`6GgB~~qAYG3%h*2VQjAV%>*3XH&%4a2or~zJ&+%;kd7YhRk; z03|Tv$+U%npGON5#Gr6dqmMC>ka(aWggE6+ScQMKZ#=giSKLF_MMWBVvg8a?GGJ+K{@VH_hai!N}XMzRw z7}QB+{JPVqJH#9f-ra@r~5+wsPk`GUZA0N;j0e zN|E#U^GbFSs%!I@(sH(ik`UJO>Ydq&Q}YmMa`)GBX%Nz1N`kvLz*UnRO_TYw&G{qF z1u8W8<0tr&{CG7IMA5Oy+8{+sD5#wjZnH=j&c*L!37(H7Xp6B;z{u-8EFcyvVn}1W zR};m#lOHvR7taq`tu0X4p?t>@h-grN7)~$4R}AbS#HlOz%yI=)Bf#OLm>Bc&Q7A3R zREgP@5=ydCzO)otjZ%ikrA%?9EOn)9Q>7d?%s{85+&9XuK9V` zFuR>z?uycsz;H>9x^u1hj&5ayi=-h{Ndmv0%ZfTaY~L^{uV%KnDiO0}VF=t{T^5#8 z4Hm3XtE<*&ukIkNAwn=HGpP}$sg8{!lzSveT~akohzs~$vIPSM5f2dlQnK|YK9_8{ z7Bho>QL_CfoC@cX?YGmdR8`?O$@a_XmQuR9c>W~}k5%(ePPa8>t6wDB^V4m%Ql4gC zZN=L$xMZ6s^YwH)(^vQ9bW0^$|4Wjs%RTCqe^auJo?)WAkZjd~I=(6N+B76gl}F>g z9KT7nd@I?DK{pT97J~&)n3qCC2#uFQp$s`oVbZ+oOX2cj%*z)}h39GpUz`fdQCi;2 zE75w9#^+9joRwI!%Jr4F3#Wo|33N5V@mD3=@0<#MBH8}bsnAMj`lD0ft7K~*c7I9> zaZ(m0@NY@BxZ-SIC0k;{Z>QUTQL=>y3gCjJ{~+1!k7@a^AB^jLC)rZy;ahaDJ7mW8 z-g*wS7tR!&artY>HbT|n_@9()54TR?lI`Up;+8K?1-N9}d+m!;;Zu&4+}}&Ke{d?? z>2N$vx^OBm1Z$xDsZ)Wt_d>G8asJ{|*cB+r-okkGp9m~Xp;>$ekVrFPz&#*wa(+Zhx-t;mpA?> z*^c@oY9j-`M<0B^KGM=Jp-)=+zk@z@`X0HzEsV5Y`cX(#p8KT!N1@M;3nO6z#y>8M zWX!=zH-P`f!su7g$3WEm!Y6f(J`+k{oj*aJN3V_lP4r2eko-HJ)X&i8i%;s`UKsrc z(dQ=%qd!5Pe|ll`ucOb$Z$7Cn=;Pvqc5{{%cjy$YNm5by4$c1LPZvf%pij8o*01=a zeuqBd&=&AtE{wiIpKm@XbRA@H)F093Z+%j~TNv>PZ0-E8FdE;bqy5b%l@4K)=#o_5 z{n012tXN3<`-PF6AMmF>sV@s7=}hjZ^M%n~%mCac^-Wvr&E&26QCo}MyI2^F{Xtu+ zBSmJw_5}62@_R*m#)2=5NQvFJuTX#CXcrp@Q~(%`l9YeFR{z4$&e!S~?ry)bR{zG) zO!70zf957d#?bm7j*`D$t5;Sa{&B6ol_d&at7j)b34d{|{;MeY9Y^yMmHfidpkjjB zV53SBJa3D0H|f8Rk{4_Bjca1xyGhj~-QO0LJ*OXFs6Tg;KFF2$Q;zmukCL`MX0N`j z)j4RJuDr0qS=#I0EfxCfwfcWIN|Ndexy12MoulL)|D!zuL@D{*f%CO`qKU_+gN{4> zw<4e}f3jA`3rH#2!CQYKk~UJLGjMY5Cf!S+0z7VQ+{WG(Z?}OR;QYZ&`XfrJjMIEY z$@zV7_MffQ{{$s3+@ybwlCUIn>#uH7M@BR(2&NvJ9P!XsH|h5%sk`|oLma1JDzY90%->lU+1J`j+nSWcWUu+;gWC@&aAk<~tNYmNeARD41Im)S| z6*)oLxwqe<5-{oCx^R=0o29_rq`$aU|LP{a z<2l;3!UK+MGrdi7Ba<5CJ8E1FAOz4OF8mcWUJwEPvtFuipwIH4`=_A)Kc5=^9`ye+ zUaEfq^nZ3R{||xwUq=Z3U!lfN|H@1CJvFxA6HELBFV)wB`R}OlH!oG%O)9rd?!I@#DDH@{09;Hzg#T)@woc`)^}3!C$XM??mNj* zCn4}1&#yqh1+X9_T|8dT|M&6wFR;)4PlbH-|H$L@)jn&H0jh`FXAwiRaml`SYsUca z0aOToC%$21X#G=e?f+#MKX+^YPkr11{*#Z}-?_D;5ou@<(d4?UUh*#a($D_`0l-!ahgNI=2K-WV z03;xcTHdee85&Zt7VPL zzgPeKPy7de`M+{0`1808I0A41;36M9DT8#8lo3<`nviltRH(-(4hpoF9hf z83TkOQcF^SpEKDGH$E+h;Kxobdn;SWhG|h_ayhRF0!>_S@v~2siXrqesdXOR?1^`* z!}y4?o2Kyi(WeR6c?F-Y9=#hPLoh0> zkVqzctOpEAtS3GcV2)K`5H)UNK~x0@BaIVsfNq(F`3&|%Em%hMCpsp1kD%eL4%I1! zl(y;n(BqC;&G6_cd01sc!ILtj_zGW=GTQ9b3SUVS5*#bo-ox)K1%McSXSV@J0T=@3 z86;E*ny=COOQn9$L23^_Vj{#YVrsz-FCuo84=~Oyp>V9K5I-$k2}B5qw#kV&i)1rU zAgl??je8;T=~W3@K~DS?c^@()>Lc`sh=?E#_xgOarPZQLK3@|a(o$`Yn9pt~$R6;! zR%-CXk3fp$t$e8%6*qzD``K(XqyETuu{H(c0Q32cH(lF#4<8q(mH5?02mmw19Oq0m(cSanW$)lM`xO05IHy z>rJhZTXiue;(H3O=b6JqWs*F^Uk)m8sQmVLk-%@l>6gcgT2Mj{>ks_{6b#WY$f7V1 zLBKhkDNF|(gyU{T^Be$0gwP|mD_4W)q-D;aqSou&OJgXaHj5JbC@0QiCX7Y}^R;=4}0U$ixH) z>~68Q)=S$?FE?FwPsdG$zYT@@RTaWUg-#nww_-&ga_uL>{>VGrHwpY1)bQUjywXS= z*Ez1TSn<%C}kML%^-jXP*kt*Ko{6CZARd{G(tP&FkDph993pWQVNhK zSVnc|II8mn#xEXtnc42{FRUQV0d85H`dNYRM+Z&~RW&R=|U4 zh-vuo7|MAs-M(=9i%)O_{{+MUBKRlZ`}zq4I8e?TA*@m&ynk$jP@oC(pu+YZIkHS^ z2x7X-0PQN(e~I>6Bmw^CJplj^K!8i|i!pw=m=*wdaj~!U5&=+2+7@>xgNYuR9$j+~ z1`^`eGc5dP58)q#N=Y9v7wk+~8*z|%{Dxq48rj;Aj>>Q}kz|2X{q$&~+B zM#2AH>Oa8mesUB4Yo4o%gb*d58tLs{NwpVW1zBJ+7p=#Ii-9?m?oVQ`QRp};_1A3; z1(Sg8yX3##_5yb?!1TT*gruTaLTd5flaBa*O$hy!hl)JE<*ejEQMiIq`)QlXhqt{P zFNA24LiN7v=ae&H-+QQjO{!&fACUQ7+sp4fRJm#yh5ztSRR#He-}ds)$wI%2y$aRp ze~Z16Q3QTQfyZ8(HYC5rUW4r{Uw(Bl^nybH8s{FW4r8mYvDdOwtFCF+fb-ZZ8)rMm zdF&M}SNd!0)i3cPAq0=Tnzl%v@8=p~`@STE4o#Py-xmKa_6pz6UEd->7D zP-ZRuT|(&3T?`{LtixR~cBOxk5E?vhd-*N)`e*w&1*hDcKaIVP1eV@hSK-J#Z+pq$ z4E#evsFXou>jSe~D)UGx>3%j;CKulJGLm_o5DLuw()O~StJVi^d-=Mb3)GW*32%G( z8hib%?FGJ{ikHch)j)$o0QiA@mez6~nDw*ZOpDq`?4^_bZw zcVR$rSy6uTsH(;qT{;}ME#gFCl8Sh5kkDXqjWuI9#i@!tpR01=2r`O+<8x%e(DKw4o z+1h$-Q6JhY^>^h4Us2vAx`A+!$MQc~ZZPX_{hH6b_m^B4LbuL(zArbp+YkG`+~5b4 zuL*U1`a`*aB}%80cYX)Q59J1BFCLydWk2>dnsQx~8{BNtybO2BLTEw!zb`kay>{u3 zqdot`l)#J~oFpo^O2WHQXpK#B z&2_rJR05iM=6UnOD*;QZP2iP)EDMD%T-Qy$z(>!hAG{Kf1??O@@@C71#IBotF>%>5 z_rSYR{KU=S-6&Jj)hjqo=Kowb%7+3JYrnU$Y&va(KQr1>NOahEf*b$UZj@hwkH0tC zQ?*C_Q{5J;{VVYC7nOkDo49_~jq(>HG3<%rxHi+B z^Gd*<_fL5eyXSj2^b1YV?|$m`btFxn5hJHyx&f^Ra;UJ)E&|hjgX>Q_ov7x zgq+o!b1ywWcq(RmC5vsrWWtGTG1L!xoJdTA>SJtH8WK>I>hq&%);ppF^D2A3ri5J4 zmbA>DRbj3$R6o|TbQb{65pskaS*ADpEaJr6uX?xIV_lUX^R?JX)zR5zik-0hDis{? zT~TIC((WBmh!fc@n(s}h6%Pa$C-(bw(hMaxuQ(2Cfl!|Go|?K<`|_0yr@}*7dw&W z#J7+b=D4;GiqTre1ra9*iNIgu6N({-uA99{;WJ<<09sEB`Ph8sw-F?bc+8wtdf$HY4?H%sFLf{RZotvg>}gscl;Yvh9g$~fo6PEn(|x*Mhbi`g5A z+6uB<=l4*Pl&sR@;%|4pN1c7TbGA79c|(;i z=-Bc}=jr(SSlgzBzDp8krU^(p*P6X|F1e!ssSyLynhz9;-AlkhNO(h~z9i|MJ2$kC zRp8*Io~bqM6#FT5j3Wcxz&I~97R#rycxgh+X9@}4XPPF~N1xsLiW;= ztD^z?JZh!k4fRzcrzN0RX*mZkTm;oA2qPe;oZZdP>^`aWIAxXH(i4~x-BS8Bv1id- z7S{FglzT_l+TINY!}cys;@ETq%!34+m*l9=s;e2fcSBw7$C4wgi;Joh29yiV3o;?M z%R1YbzCDSl!R2dKe7!ANZVG0Oa^Oh>mMCA>pu(OY5?P@z_o<9n&JSa%%hTt4+(@Un z`C*~yo}Z#|t+W9bBUF5f1fCYxUG(L?nlA_Mz|a^%eC?PX{@H2IWB>6iURw^UGAV8# z6+{vEbf422(aLQdXo&O)wI2i&+~O5SKo_%@qxWG_&&X<^GbE4$JOWB=Crw*hop(g zTm30+zAX&tfOsY&TErK^53>wV?>zPjcBoz;7L68fSVg;Zb6U4kKP!d@Q(uM4Z=cmj z{B47#Bm^DCKKbCdd-8SVN*dkfia$i*lHwVz3P3Si6kjpAF&9xh+z2x7s%U?u54`p4 zjtHrx?23Hcex$PS?(+i!`0EWx&}uJvQMcJw)Qvi4c&Zb(5#ERQnVK*b-Kp=LOmD(b zG!x=e*Ah3m18$3e@p4f-8F*qy>!lK+gDJrYFLwGEJLLD{rwPH=fli;Tft3B7#D2Qir;prTFaeY3E|@hcrcA$+uAUNGMq^~m-|QOyXq zX`In}7b%}EYu4^*x?~iib1O$vD8gEZ#Y&dg<@sI!Ubj5HRwpoG%u=exVR*=o3GT2l zq#I@}h?2SrY6FYl6&Rtij&pgFpZ1(}SaDdy>)Mda#d6jm!hoz{0($I52rU}&drWEw zmCYs|1B$pspY^7+@ncD7?Jcq&++w>IlRhr$mM|^JO9(m&OC^xD;LKQ+-cX#SX@V^MRyBI+wrpZ* z!mRgV+MD)qYC7q3Bp=1z2N0fVnjofz>_VoUF%@Be?wAA{C)X?SpL(w&*vSqg#@$S^ znRx_#9SD}SEm3qMs~L*oNPcI~e^=JWIU|X3KUeO-hH_)=XxS^f{Krq~q%YNt3%|G> z*=A>%BYZkNSFKW+^rXP*Y29>#q(fzGz#8GPOnt{IhZnYG^G)RmZOpE9d6+Y9-f}K; zS)aGPsDgKVg5L})F&77!PVYYHcL_%7c1k>}_drK(SVP!29td|`&Aw!xzFi0kG5x%6 z`?>bD;p}MTkS6@ClKfi^MW-{Wqz6rxZi^ztd)9~#9a$GW-VH8tS-No+dSCaG=mE#a zjSi=s@yjl%w&*fl!@;K>hjV9#*uheraZh*sE+=nLc{v4aj-PBZT6M|H9*;cz{NZ-i z({=HOXCKc#=noM+MP#zCcX)g`Rk5V}@!h4z4}9FP*6do5?2+JorfWRMf{r;lW{va7p;RF}xD$wQC+^4pARlG`FuRw%b2(a*;8BtJPk zy_Nm+0M+xe&(qtTw$>}o{L!AQdXIKOxwOHOvTB?ukErE|B^8PJnGW!j)V!{QJHQY!h zzm#g+8KfuGXX!(GaeZjL@k;Mz^yo39jHW1x6ypfC7g5qh+|1HXkRC2(I#jk zI%sqJ^4qnbchJi_(U-T?f_IIAKR_=-s{=j~UpaKJr2>W^8ruSWZBa-t$|t4P>hnB=6ukaqu}dLU`_zM6Mb>OD@;P)u!D;d~Slyc{4f$D<{P2_}Ry* zry@+AqEICyq)_~nDB7AxS9E4(l4#B@$7d4Zv`Wz)$B&=ZLb04}3lf;NgM1)+A zv0c>d8jp_6p(VfioN>*O3FgYMW~JF>70eiWY#*C@95dr{>+-PcR2T`tLPXrXI2`V$ zD8}}tz$i7?9=Y9jF%V`Qa}Szn2tY?2u3mB*f zo5q+qnO2=*!az;K4fw=>n~WC&E7JTyDzYL#3xHvaFA>H=8pg*T7Q?{#0GyyO7m*ar z!{P(H(~WzDKNX5W6s3@?n2e4U3m_1~C?ix*kWbbX(5_X~*4|A}5|n;`P)%!e zVqvqJrjCmoaE1nyILP$av5;`f#14%ych0dJq;L^4O_w+2L`_uJ(m=>mHYG&|C8Jjq zqk{pd89evUo`Gtva7xmnzZb*Js?GKUC9taIv8dyQlOor=zE|#>(;7<{i)Y~&3QWf} zk@AvI6eMpj5b7q?`pkfwKY&x2p(7ZDpHRcYOoR{BQkaAwD{>YpX!#mQ;)Zu?^G_DG zofKLQvek6kq&HzC)M}uEH0336N7^kn2~6{o6Y~|az5R+lI%}|r7kDCPs8gfwi4tQ! zPT%G)(Xr;X(NR$B##o!kA>!p~-N~YmBL8f!jUEY90%U`y@D0j&PI!HTy0fb@5+y1N z!-|oc&Pa3E7)|s}lJKX>EJ{*k#W0ALZ4RCDKeA|EkIYV^Mj-Rn`&5x>w{s5>Dc^#S zAyhuS$&W}omq-GIft4tM@Zd7vTAAJyzLS0`Q!TnWHLi#h#u@=RnHUCEZT5R!ZCMpe zu5OI=>&Y5;Z0J4Nb)$(~tmG2jg(x@7VYwN@p{4p$)g0EOvV^9Odnz>qaKiJhG4W&Q zZJQ!6m?pqqwRwkNhFTak74 zUJMxJ88(P%ygk*1%{*@7Nkhv?oiI5Z?VQrn`deHE1onlWY&a(u*g%{{XFNnD>Q0wR z9_pSk@LM1@ERi*+Ynx5kznG6CTY7EoNLm5RqrJD2Kfh8){W@wnuJKX3EO~Q=F{tr` ztjPs6w3F4i6Vs;EAD>l9S=z}LovjJ?V?)VAV+l>gV|oryd^5i;_0WoSrxO7n9{+P3 z{iR+b?42gk_?CPOgK3+SC{5^vk%3asXGHryu3oFaarEUYV4(A>kjONSnQ0UVN?ZUqcPQftQ+<0|wx=UREhj>)hh@tAee!(*_9L{SP zLsFO>e>bJ+V8aVQcTLya2ZVE2r;FVpTB)b zde+l#G(G52uZ?C-a>8%k`1)ZOAVbeeALMVuJk zb%o;m{DF}^w9vbbo^L`|uw=3nbJ6b~%oGr;Is}Es;t#yaZ1{Y} zgTntgEr4{faA)OpsPDm)au~J^@9j1J35rMNy1eZerYd7Z;D^TTT9yZDb(=hFS?JpS zxJJ$mo!Mgy!VN7ZtI@I^b|`?SCVBxU7Eg26&|Xi>i(*JxHQ#cVEM0TN{d9K_xbqH;26?I2L{R`EFV>f0YK`nQPF~Qm z7Q*_7U7PxeEHjFan6~+08bED{YP&TzL6(p~v zsAE=P4nHI1Wbc8kUN2nkHsr+2*g~J26zRwG=bn7?-d|#@tiUY!6`SyiOshgtYRW=_qz45lAc7h1XX$aJQ#MHzjvXnRc58 zj9zhf%#&w4iFcfv6xp%P2K~LMuE0F>-qeG#jd|IIVA8U|dxe~bX&W}#0g{#9C{&{4Kfc8X|N(e(iN%eQmV0GscMO%$4N{4Npg-X=S`jr-)*aTU)iG0gnX;#|kz zoHJPrG+a91?#biU$NF&a%30ND32B%*L#T663?EV+oZHL*HHJHP@1yny3 zU|FxnS%&MrHnqvCVLLH)eNVh<;oHiV(Zf<>V|rA^0PxAIN$Y!e(B8<2hY)r)Q0)Am zm({1DUXq=OrTC%!ZgY`wMVM38PS)p9j8`jWfc_1-LcdP5cfKc{u(s^T@fQFZfjI#W zWNb>2X!-dTcL7{ZPhy1y5kCTU6<`R!SIk|47XwN?m*N(T9*T?Qq_8X*$^PJlCyCOE zbS#hgOlyqNs%+vd)WoYWh)@KYL@KU~V)N-cx}pPf;P#jEY;vA5qq-wA?U<1~16>#_q1h584hp$wTQNVUTpbQI%WTwyiI!KT0Wnel9Ug2ti6 z3@Tkw#GA+359iwgsl*fSAMfT?#xz1cPnV0Y4i!A?NPOV9)YnL4bLMo@(du11yXxg6 z%}>q;K6F*GPjFoyt>8?}b|gJKbNw`Pe1^&?fPf0wuB^j=WMAF|suedsBfpB);sJrc zTD&P+Ia{z8r;A%~E{_yb2jN0WMDXOKEd}vwPRvA!9(wLvttlNOoMwECvv&OxH=%i4M~bD z%raNU@&jbTrj^TNuFu9_k>!71enpP&Fdy&Ll~W+T+-+oR<$fVlo`^GfQ9ONo1@TMl z<&IUP2)0)12HsjZ(hU`6sG&x_BLdb(xm(G(9QXi*WF=Ie z!)ZCnJcwj9#xC=%w%=2f+E$+@nsQ~yswO)|8BtsPydM4rlc^+WnYCvoo3NSw+j&(h zwQoOSDJk3n0vwIZP%a(Ru4WAz+^8xBQBI~t;yIsk`fMfUZxxnU)LAxCoc663yx!`* z4RqtIUhurTyJOiw)sM+jWHU0w3Jl@M&$ zq$YcP!nSg2Q-ZCPR|%k~ds>1A5pPs--cExengM;Am>{@7cM!bqxuK9Ol-h)`A1Z5?jwu25$@T=! zWiW5xH651YHpKa&G3xuj8Q&7mOfjOtV0M?nY$T>7Da@c#cT6sEtrp`IT5_KSL4ox0 z^>F15L6P_wLwhkgb36n?11_a?mI$IaSv`ZhwOJuFfLlQePu0goC}-2fWz1+?H>X5S zh-*4-$I8=}c9GA>>RjcbphpMdcWP{`we6=!0IZD16y`b`cwY}iUX6f0pjEh(wXJ|r z!KZ%%{>E>`07UQf&Lt+b6yO4AM=@UmZnHiGOo8xBb5gEaN*vC{g znjixa8<~t=3UlM6)q0wB6GPr2n~)hQgsHf;#2Uygt8p-##DGpKL|g48(J30#SiXom zy8uNnntw*_LeC7r1-OSGCM`v;uwHBSbDl0E@F!oulo<5OhPso#dwCC?-b1L*H(h!& zU{-!|w+;F+IDuoN1?SUR8ySu#ljTQ4`6*&~PplHyCS-w7Ed`kxs-2@i-F826yp>74 zoIPz#m!|^|rLa^&c%oJ|l&xko94D@QdL4oa0RL7@W% zh=}MB4SMq7JohM9c+(USXqx?neLWy2N0F-CZCPh~IS(IR7h8QNRou8t!5%mg1;q{J zP%vs^jxteIOde#;muJA2C`HZ+P%a3g5M@nIMphB1A{3c0;=RX3uu)1I7WO7vT@^9u z#7o5gNEieN0ypBka;VgRodl`WYG%!?%wWB$&@_D*nyT#Me1c~cFj1`%O~XNF zWUK@3N)rq5yc{*gb- zp?x_-Ln=&`I5uwBcKPn@%Wu;wowoI-qVyX~t8;3vy}PvvT_ERqxHmb@MlPTA;E9>R zwNw5!*;nEEs=WQ$6BA<`cm+wrEAlIS;g9R^D%)f-Z$&YVLNW9g+8ZqF`wfN<6?l`U zZdp1zpiFjXS8PVU<5L@(ngtG3J~C@iKkHvVnMLjBuftT)Jv1>Nm#(|kLO&Dd_KX(AeLr5w!Lmz2qwi|U(7|n?!F=x?uz+lW2KVtvs$5DWXY0yjFP!! zsElYl@y88AIk}-v`JB_&NyRCm4xf>7m`M&YZYT;rp6S7Jm$dM3+xLV*v^MX68JAw- z@y^6-gVAxfmKHVz;}zC4l&v|InK)gR-91z1(jw}Lpzd`uqE87^#VZLXrnjm3sv<>l zM}0}yb2*(yOFM2wWftzh7_HxGCAF~0xJggqqNZ{=5Id4kL4FQJI?pr@6C(F zF4`Vcy<>AZbg_(l=?=Cc{^enADA+_>oH>NXbq0NjwFBYt{W3^|rRMR>sI%c^@2r&7 zerH5l!6lrN{nLtly2jlgPIygI^6j_60;Sva&q--#_%oPV7(r=t%+bj>r|4HtZec7H z5H7HVxMdQ8OVUDsYbHq=QQmFW@28CiNR-fm4y9chhQFajynSZeag6dKYqftgaq8%s3I z**PQr?CiMpI17!c+^vLHTEwuhLv(q@ow{h6|ndrXR#NSO{wvPJx;WYjXLtg~>! z0dQo8M0?nWAdbQiwI{P>p!71GncyWVh+ARR0Ea?E>jTeLiA_<>M|tesIIW=>!|s?m zB58*;MQdr8j?NHfx0ESNy361fpAbE;Hq&ad8SgsBYC&V?%ozwLv3&FIwP#h zis?EwjcC>-g-*g~#?-~n;wSGc=47G4Jc;si2>pNne7B%!;k9Q;fRT#1E`BS$u4aVt z(bOA{!n?|t3m3RBWbSih(^4zKl0bU!`cPWtVx~Iv+=l9Q`)JxE+Ehp<1c6vmBLWd! zz~g>Ih9169bVN+_)hwuOnXO`QwM`OEENwI*I&~}Co&={!ePeZL9#?9Cdk7|QAy0@d zAhNv2>SkyCBa-Wqh=Af-_AmhZ{rtE<=HAUxq|q@rBiuQ){_l@*tW=buc4xvAxT6Zi5_l+}I?+1CRK)2r9V~ zy0{9sxb6<0UDa0_l+Nkc8{#vkTbida(dai0Z!NqUXZ&)QY+q>~IPw8sS&MvRA2M?D z)1ZiGXo1FHJV`WC#4wa10p(pK7qUV$Bx5CXK*Lta!*tk-tpoe+fPxIPIBLqg*2 z-MnlCM!Wxyvb&0Es|(vk4Jqytv_K&wfdIk1P%OB+ySuwnENF0dcZWiO7bx!TZUssy zr9exKzu!N`xA*DZ=j&*WHP)QtnfJP{we~bEgBUGCSKaS;Y6fF#dJnRURckD+Ylfy; z6!)4mp zdVLRyjYs}fAHr1`U41EtzNHV{i_hzU$pCGEHXyBLw$o(DfWCI}vf7LH&V-s;wv&+* zYi|h!#X~1e@OuJ%HzTQI!o>fLP*L^-1B-*N*K5QDl5HTZMHw#@$8|bA8 z-OR)+;!!X#c@PQ5+ythtM~rRc>#pZ^>8Vo~6rF9kR2fq0Y?UU{YI#h0hi+5|Y*)fJ zSlL&7UG@EZjrQ5~7rx4snd&M=Y=w1gh5p&d*)z&LGg6h^)PQdmkF6`i*P2{)JY=LB zTu0AWHc96;6Kt1zowr!4HWg%6p^&XX`mI!5ssAtyEp;~+aK(GzBetsRe6I2Krn^E- zJQH(UQ)BDXMTU8MdZTM54Noi84<@y)0*K`8h0yI~LY>McL4`e&_aQjMaAQqznhv;p zpUloimudT$sm9tK7Gx*nLFrj8i<-*RRFMJ?6(Ks9)nkIG2YoQc%A-|VTQbiwGq>Hp zgl`S23T4voe{P~xU7McBG97a@nISZv-rM^6S;uz9jCZks4}mW z)iO&mKj=L8EwE7+DzN5i{vIQBdv1?dU~>n4u#vo|;d+RzW*lm|gA8QuczH;bOwVd+ zb~*Z;*qwwXM`V%Yr{5 z4`}<;SXTK%@8UBP5x>*dZ(kk$D4CW)`Y$&?zroN%`;_@MIuk)9`n303-=!hhV7aNTP^5JUTH<*8^s za+z=~WoUQ27o|`#(vQ}+NWa~*5nYCZf?wXz6#kT!H4RZhuQu-v3?49zn({8FxGqRfxm|=*5S7I}Y9ZI7@@nCMiSZ{tW3f~HpCy3u2#a#N;@P*l za>|jsy6v}W*0GCGew!n`)KdTbQsWPKMH*Evk^sL-j|kkwpk`;66dhY`XRR4wQ~XhH z7geo!2tcTD^x{-T*}P2zoH|iVxUm~tD**8E3a`X)x3+PLmP-;sGSKt?OO9qi7oI9y z#*yMk0*t^38=$+RHV5ra)TVvoO^T9HWpIl)Ba4_+GNr%Bs4q!ugU&Hq)sk8y{DOL} zt<6b9&hpRvRwp(|AiYUfs1xgv85_8@zHT)NWl-PeK(~@-kT>NxwwYMke71gnw$c89 zr`Dd%XZUZ2!j8=hYd+)64fwnQu2X!xsArgwUxAy@w5wf$0Mzx2vx!CFBb`2HvNr$8 zP?#EF2~q(pIF)-gtFQuYk23M=7FEE^lf_C2Ox9L|jH*=UgnQuln;SQWMuKfx$=(3v z?I;KhK!gA5EfJZRQ!LS=AEJ?mphB80gjd$u&RTw(xgwrTGOE%xJ>vH3C2E|d?A+BM zsOrqA9DFg984;z~TC*e#?VS`yr{+SR1fMp~vvJvGII`POoE;l99Vp{fNS0Kz4uJXw zo+m5R%_u9%78@LQhpB7^A0nOF#F%4VTZ`CyEn0aAOk}dAZD&g@HIn2@cZz9UW+ef| z2tIlXfMFyOAN<}mGJU8&68mqloBVsX61vyqHo18`T32jiz+*q4@`H zoj#4fUq9*_*^soI=8ZJ|K-Tn_RPlG@vEnGAJWh48vM=Yo{XW^iUX{fuh%zbwFof@{ zwrZHthp&N?!rPp5?R6OMfIRNPA7#s1D^rrt5@=M&8S?v&4cn?4P_fGNAOw-lW4we^ z>7WXuy=RrqAn^EzVNJ4~o!()C`muk-hJx@lCE<-(oYqVSoykeccCU&c&W8VuXM(c)JJHhijhH#biK-Tj^z)W7$m zV16ULd9_J>vwy(AGFs5G!?+Ok*hKnRP1O^$ww6QxvoW-zWKFN|!6-dq%OWwjrRz2} zC!q7=uV+c49?^oT(7qvivmq+ekqK>6o$Zt^lqx^`K1N*<%lBr~D!E9wS7)wIk>~qoaVTmZCobbXt_rHb8 zilW81!?6KX8@S;D;mwvmzZ|@qrh4vd#d&XB*F+LbENJrB)7Nd9a`sP!#l}GgV3Tn; z+%k?t<5BUtC~!x9h$10WOL8EI9801V0u05MRz#%YL|E)P+<~Vv+4S4_b1@XhvU%;7 z8y&wv=L<#s@2)Suu`HIdrqO}xNhT#L0a;L+gvATBT0VI#StVedc9~HR9t1CgeY4eS zA|3Ah$g$mNNv@Fm?^*(lk>Vo}2h6j~w7{b#HuF7y%l1$dHaVxu5ANgV(F7|0y_o=Z z09Yp(5a#m4dp?^l*xuiLTxoHC{%iOje}li;=kV8IZ#DH7m49OfFh=3BO_IB_nd*}0jo@OC>-4wz z>*)@Q2OLEiwP9LHUHJ-wkdXX$OK|~ptdeBb4kND=w30w$4o3sfAxFXNv$Qa95o0&`XM7s6qv9y2OcGWYYN{uYh{1#XswM)dIYe`p*;qS+bYTqt3-9kl4+r& zv0$Y9tdt%qp7C``>8&Gb?3MS1nP?wCui;&5QCL|f;3sMmX_B_CF;?f3(E?=)qQE*JshzX|n z5t^hfys@Z?B&LWKEsxn0U?cBN3Q(&Pq&T68Ih41pe9vD)Ar-l2?8 zBhHii=?~@#3jU(}GIps)H7OWtxI+7Z#PCzUVMqM02l}e(4j77HQd}C=R8V9wW6jGL zN^k*36|iwOo@Fv}Te-eOZ|ir@N7Ac3lGCu`AFB%$4TGY&>5*^9hDup(MM>P;zM6ji z*KnVZ325c5tVkLBWlzA#ak;q*IJ0VntLvx%3e{$uM)V6Ur6paREwOlu2X(CgnSw zUc^afez6E!@U8AB{^n96ei^u+eNJBT%f(jW=}Fxi3s`!0zsvhy-C~Sf189EsRF;UW z1gXRi0w%M{1?DYXXL9AzKqh4c*tAf3_Z1}cmx_u`%ck|fDh^eciaFbgT*7J&!9X3H z{ZzdOFS7>g0jA;4wpvW|P$NsorfJHW_aNH8OA5yM1hIvDDw?lf?`(fwYjl-uFh0TC=xvv6_O1G4{3S?lU?16nIn8d2dB@bQ zHrWy}(qM{SusH_mZVT3JwEl&+HNl(Rp7zJt7E)k1yPPH4M!@diy{++@aNQV4+vL)3 zwY{9K*VQEZ*%6mwr>)wr3)l|#abn-u?iTFnF->>EmEGRIdF&n?bM*|JG2R39^iG6w zdYAn&{wUYew~}?^)#7Zr9^3tX!6h-^-^lg$v7tWp|%U;9QIaDzZT2Mv!2quJ2Gn~j~}XFd`yZIM6>)vI-!-{jYgyUn>hSY z;JrKijo?sdSEJ`12d~{uRP%;V-jbN;7M=L3QS*N&a7X1b211D}R)?0+Kn9wvP&&8XeV zVo{-jam!hgzY@RnlUV51uKNxR+L0%DGQ|K(;bot(>HJULS1h|((?yDNj2V+XWFsY$ zic>!=4rlh+P;3lE<)$9$iROvfl6dC`QGkXaBRu}enlwHj;zgWZY+5(gPeEjF<9M(5f z-8kBf&}z+0m{+!S3^b$)vo}QC>DwJO!Ui0JdxvcFxGS2ty%?JwT>>*yzd?6I7ZID@ z)sT<(d<9@o&}G8@qZk2XH3y8U{8Su4{U*vviURW|0C5~f&(orql#G~0KHYPbHZJ+J~ zm+Q|SB%@<44)3GUOg4TlgYw4dUjn^4Cs|REA6DO zZH%v2o`0WN0%Rf1DGv!^Mvt_XptLbMtq5#97NgZHpsAFp$Anv1OM@yPZz^T2UP@fFu=YUB1yVHI1g_WQ)^ zozV`sg86A|&!WtsyIRg8FLA88bM9Q)Emvw28ta=^b9wISrub1dzU{i&Wxd*C8R~Xt z=SK9=lg-|f%-;Qtz1JPJOCQvCojM4H)2f*&S*$UkTjMWylA62h7se8}zU+cx_KrX; z>6|Lq*#v5_Bn?BQ2s2;Ho`t+AkT_yt+h*x3Gz{Ih55u|$16)M>t%;-rM&7YRwXgUL z*1SC@Z;&nHQMypkWJS|+C|R>o(XEW}bBI;Dh_kv-GG#>uUBu_tDlY*q#90$54FkGa zlirX!=3@tAX}unI@CDR{R2x)QVW)Dk3b&C5gIUuUSVIAqY0&4oFt*zCpBY2%SW@y& z(+nNdE)=58QY~X0n9*qyC@bQt9de>BkWP-dVRbnpOPN0Qndc7bqhu3H)xLsumqg9@4HEO(YCQJ|kaF9Mis~1123;(N*#dbv^by-DgQ>B(v z&?FliFMzd66(TSekg2H1*qXez)|1x^rYoay~br2-s+HgFC$->GYjnsUtTLO&rn8 z>mm=T)NH)kjj*ho#932RJ8p71CPa{_eBO9cIpZiI%)1&qMc$@Ky(T&S0!FJeP;K&Y z28^1He^y#+o^CJ2gW>EqB+>kkt@?gF`%sifFkibA%dFU(KQOv?YkKITgS(CbATZe1 zM3pbo%nMM*6KKcpiW`9umV*RlXRPHnK=ZSxXQ!#c9xX}%2%431u`BmbadA_ zshIr7CTHo(j8eS%-ziC%gp;iJK!ql>$a)&1(ydsI{;-5BM8rntbVFxTQxfCWGSfwU zLQ|=^ib7NaVn{&Ec~Q)wX_M{R>~u+Xcsdkw5O}~ETbk53GCe$_m3#f}v~gCCgyarc zbd7s9I69#eY0{5um^~m5YEE)6B_lXULPUaxOqQxj7mU<_)I{A{h$TPw#5 z@sM5NxQ)e}ipC8Dow{#QzI$>pW%@Hj1q&2+!@+?`Ci}AuukZ)*hMVEtx>@}QhyzLf zU7B0q4R^kDCO2xlY9m<$ zPU&7%zw8+0*5hHa*U5JybsXXOWRV8_^Od3km*#alm>Kt=J(?yp2DD&6uig&5xnRI& z&ERxQ3-gNPbZ8WGgjIO4zP)2@@M2GFPdm1M>&4q#?3kC$ns`Ey`X)0^9|eClh?w(^qBjVK8Hj!SCce`VsmLO6^6kw^huB7k$^Jdg&-5YaCMFlOdev>k6~uK#UroSZTmLO+FsB`==sr3;?c&x+b+D@!Q9WrTF{~7(V@W4DMzsT4WC7e8DDCg z^M8V_LKFP{@&Xkk#2bEu^_#*i-$d+3-CsE1=XD4X`+G2d_hj)8z2b4_tLQ}g##2zo zE91|%!lL?oco}i6!SNyS#zX-SRlfahmk7u`D zSal#N7B8dd;9{>()}F-aQI3@!hUqrMRmq4+om{`an!?xk^0=tD?_nfAh|I?!Lhlw$ zgkxTxN{Gg4fg@w>8wlH~PYHjn48ihJ>h4Lfrb*9uhoU2>vCPVVCP-qo(dq+{xSG+P`b1PM zzA=X4B)<~+mEvV)AMusq0EY*vRitv^fU@<)7Y+4YH8zr4i^d&A5cFvEJ}7g{KM(`= zfx{|0IJ*_aBp=sEjHh@se~|z7K?`Flg&mO)90agC%>Z8wBJkhc)^Fw3zfAuyai>(% zrvya3XdShzFnFE^sc~RhlC&U{fk|&MT&^S?0Ij!Ipp>^~lS&wmzaH;|WugPT5RAcf z>>UkH*dq&omzk({eUdnee}FF8GO`UjaN@DuuRfqtuzRf&D0ym?G+oPxnHd%91INC-iTVpyDhM+DZCA6Wvr^Q<& zf8>M%A_~Vbia4d{m`bn`*71>y3H9fTOs5`6z4bpgUjB*eW({~7fo zfv|46O)U;rYf3m;1sL5l?JqJ#^0VefJjI{wFWyk27-Mf~36RmO9(AB3{L>XtnoW$j zB^^6ceikjhW}hZN_MJ~*Wh+)|EB<{*fHnq>eJq)12<=`gTDzYfQ$hV~k2y}6-jz{m zybf8_YgzzDir0$tXDUxkeldti2B$!#+sBgLiA@NHvO7e!0_$HXg}K)DWy_3qWSV{9 zex0b&-y@mw;e}H}Md&e^I7#Ur{yCvf_W9TCZ(AFKCvUF+>dBat2fZ#)wd(Ymr=hJ_ z=#mJ>8d)WFRU<5DU6o=K8oB}o!m3MA8VJ)YN4>Q6)WHB($SE#$asyym=^!vE+Nh;+ z4wvPi4NZofYJrgF`D$;5{S4Sh1EdHW0wI!!AhMD1-~^>O#Y$laErm58k&Ow8JiL2$ z0u(9_JOW>5yBK!ZFIDTl&vrHLaX%W*dY$8D`u;V!B%;7ZC%wO*k^~}fWdj@{s%iz% z$x4!Gg-KX_F38c%#3{ha7^y{OWn!Sw@y>-9OhdqYSlX%Mg9Cpg|6aULPULPz9_WyD)97iQl91{3X zG?7Dse_LJri|C&3;lBFwq2Ly?-~34<{O?a9dr+e2H=O>Kp%4g6MWiT$Ii804$Am-sO{yQ+YnmYEIAx zUN6b_+y8D+sf6W)oGZH3MHy06oh4bCtQooD zL&R524r7;0WiI%f&WfstDR8kpwv%`$`%N<6vf3*Q{54%e*R?gevzN-V1yF&mMSagX z-3`+~*N6Yiocky?Es^l`O>6VUu1%#hRgx9EvSd9YHDy!3Z9qf6-i~YU!2O2R!F9Hn z?HGJx*PFDF(!^)Wbz{$;6yk4SchvQ1E9mQ-{z1oTm*2rVoj>}Ak+{#Xuyuz^xWQ2j zjazPwdm6?15favHU=bLpx@m>Wc8~~^Yu8LpedM~VE=&12RGJkgT!VIm|Dav(el6zqHmCc|SDU|;YR0ZO#Mva!>f%p1LeIy+1w2Uu{oXz3CF`p8xw&##qGc!zN0mZ} z(JxD5_*71FH5(}=UU#Al_(+~j%a}~K*c;M;WiYB<1}&FXHi}VOrJR?@JBoEkS6dwef zf*v~*XVP;dG3R5P1=2=@yEQ5={;0!9(TF@ZNr#`$#+YGSj5f62B=7~#m?-b#Ls6}L zAlMii7+i>XCIcoDlw&{UGfh*Yrrk?x@^7R~2ZAg?UPF7OSG!Qn)q$A~M zK;feBQh?6(5uhzsmR9!$YIjU$g#5$yUGV5Ee|>Gsu#G7FIL8l2%rMhfzQ_Bl(2O1aXTSSn#BT#8rT_* zg{0BqC}rhgfrb@=i=$I}w~{&HAl`=HvlN>4JQe1hE_MVwZ|klzSK0K68&2+m1WuAp z8$~0?8Wr$FQaZ*px}#DGYB4SN%knTVGGOTrm~y-Z@~WFGxEm_cuQwSn`BIKpu5swu zH>F7Q{^ejRG^}4TPv$2!Y6G;WUnrrhGgRCX!ry0rDE>^)y?9iI$SOvFW0^4(1M1kc zlcJR^-YFy1bl~qorl=&qgJ2h?qS38UPf_bE?h)_!CP`J`MeYcG_juGJV~mxY6#dt- zY6{~?e0mr&CN9A^SHUJiNc=@~a5hvhh3Vh?pIc_S;d6$jS=~Wey*ID{1B>m$Fb->~ zHNBRZ4g22=NE)IY^X>F9M1qM1@UPz*DYyvLy107M^4&vtDD2t!BGGj!4%CYkc$6Y#PvAJ-A=(ZvQvi9!)cQ-j-5> zFKYURhN`?I&o@Bl=;n1s%so@47rpVr!VMXyv)7_l(o4Dz1TbP+(2-NmkjVxaAfup@ zb?Rt5U79U)7T>GW^UGDv6t+VrJ<~~^$D)6pH6Zy8Ntm=HYrU3x2>rO-1KTx6I-$5| z@uQ$A$)jnaJ6@`-c!kJ}XyTq&KI_#{*C`9D5|k7>i~`L3m)BH*wyJ-HJ zmbDMn1~6kwQYd`ZdukWDnhAZLu2vQGbaJ+Gt+DtX=tgXLRcl(+V?09(7wofbC>sz| z__hUq^}_f1hWFILyYiwjUOw7<6p4PDcxOH z8|u{YaO3;yrsvnaYex8Y?t>TqJ%&k-rbm;T9B~`{_(0F_bzUmum`s9njC9^TDi!a9 zuIJqZ`@V>ooyqBg;8WD0YgPAN!r5y@5u=OXsoYLFXp9x@9Ik+O(MHHek&jOcp$tCC zeT5g2ulkl^;0JFGex9-T|9oFPR~qd8`%xw>itGwTtWAN{s8F>Eup*a|GN%Xz<%by$Tjo^dc?~e_{Z_Kew_jfJgpMqO{?Y;>A(N6XEOEU5A?c2d0!z^#^ib_H*~<4=dzf1bWlJYcHQ|M`5Nzs~a0)o{x8@MB5OFZ@@o z4?C61XvV(&kHj_rlIO*$OUlXV)hNGH_e;C6U6q)!@$OSXvV7%pNMA93m;3+Wq%qsA3Hc@UfMS&z!70~gj z2;+^c#2G7eo{K04QWyJMk7tE~B10ghuf9BCN0)rJ)YelHJWn=E{`mMPwfd3F*!KSTAYjzSK- zXmW@uQU;H66nva`zg#a)XC)pxk&*HNS<3^inP!auuzJNo6Eqlg@1)XOS@`Qa(JFGK zaoPBOjQp0Y2m)anmE_Dchr-(YAnP(0d|~1CR68U~&n@hXS6VPxn)R-V z1D_YintSw$=NS~ihr^u7zzajmNZGv-`oYFyH?{ZU=1aa@_9!lMt$jR5dm|peOP)ZR zC!KAOrWfMDXwZQ+{35Cx^o>B1pI$Cnhm?cy z!&GOKkfS5(Ac#{++bG}5cDI!SZHRlmK_t~#s2=-W@!}^2%#SH*7wJT=b8bX?`Ch;G zem%e^Hta1%LvT5{B0AO~)}<}(TBnh(ak;52cA!0((D4Snl~P8l426SvwD9H+x!(DR zT>2>C82t8K6Xyy*=-FG!@#L6a?M`7{>KCDOjyuGp<%pYKnup(nRYyj=Q%34uO4dhO zUPoGW_2gNBUsj#}L0#%pz37n(eNV}dYHUD+9!g-;)whDDnBY`0K zR%dn87w!#L`7d)ZGnUDvYEHLF_?L$|EljxO){{S1qQvNZb(*R*198@TtQ=5iKlCUx zo-=hUvoJBcKH9jsZ~7<>NL$&s{T`2g%tz@vTff=3x!mT^OU_~eA7!b)`QsO)^}Dp3 ztaYEOd_}8uzt>}^eYSbx)OqU8GP_%~dpagUBw>yCATP7Rb!ZGKL1 z-9kmN&d#IG$Y{r5zqj4%j_Qvt*3x+84=_6bV!ctcmc?y}#qB`P?JK)m$#DoyQ|Pm> z&782!chry_I@n_l>hXzee({B8k&@{_k|))7&-d6~I~=ogLS9|iUa;>nwEEsW{@z?0 zUVPuZSvI_7dVCbW&$YA6YEk)$P%R?-eF>X<|LFO=qI7lLU=ozFrBOHjsB9dXV;s66 zz43S((Gx&H6cD3tFqp-8wfaqTL9l=-NFw!{n|Dw}PMpevpRN}^NvR>^;VJuz`xKYK z*oMBu+lRk;;{L=x0+>TSY&^07CB1wlgHM$LN5Up5c1KRNt-n(I@gEk{4c6!7*73Wq zT~jIV3MlD{$kP38VIOLo6-o6@*Iu2==ew@DZiK+QohUqmKmlWkfZxkGp)*1T&%@tC zL>xoJ4Pp#)LrsMZ;GIgU3uHpM@);D5VeXN}Uko#Z0^*{LtGbLg^o@ND;@LC(tG)*^ zw>e$wa86_C@(CyK1SFVtB~Y!2wfDsKcnT~0h)<&mTM`Phr%bAg`)&6jc*)*vg!<|4 zV?f7*%J1s%Jt+FYvC?N@<+eyhzW+kr&S)tr?o_o()@^vN3TbHLSr0Q+h7LO%q_BSDM!>81VD(@eioX>y9x9M7> zOakw+pYlJS!6!clAAStS=f78q3Cr*v=FNiJ+4zNPq%X2paN>J*{d%YQ02XcJMf zy&ah3i2{@+M1$f>K_!FB;`~z6`!lK%5LSZffFCOG7m0L<)&VAolE&;@?(7_YX`o_? zjnzslu@2;dJmrwU9$MxSh?KvmVcEioFiwiF2RQ4KA;FQ27HF7BC~Yjn9>0~B`=>*5 zu63@DsDxY@Wm2Wy#iYOVmAEf+Oz{gYDHzuyZ+-v^ddN~c#|)LPXlvNEa3u@Z3%1Zk z8M5n(JkVrzVbiC-_T=*DgBbd`#?twt0F^>E0s7lfG-_#|r>YmcY|o#SlE9K0e+KJ}rfgPEj_Z>0As+vQ?g?6wgKpT#;>D z5?xY{<4s6+1RZqDw_RSX2qIlyAy5lZQ~I!K zPta+f401qSf6SU!hDn1&B#ybGKJW_Gr;sQW+{lITB*5C=!(v%GQzwgS+q$r5amqi| z=s$!DMh-cAZLLcvG^ z%BwbNX53YW8fh4aJ&|B>sW6k{?FH*dpa~#zC{Qbj!`LK zNyKIU$vPHo!fqrsQE@!sA(oXMF}tM6%>9LmMk&232(6_DRLK1sl8d(Te+2a_ zllQ;IeHU7xzyuhw=_PaW9{u?KwRO#W15o%Y)BH@76@Be|(7$KU1?AD8AhvVkyrP%` zCR0ZCrU;29wa%f%GEgqrTON>9@+`Ygf>CRTn2&)v*JN{h>2tlF+eVX}c5Z zH|`g=4*h8#;__!q>#fkZKV;L`v^$ zZYa0XRZ02guecO5UNXJNq3HL?3?D>e|I3YyDa<&eo-PF@Y|vl#Wv{4y1!H;sU?`xn$B-StS5ib36?hg{Cl` zoKKLx4Xm=xgjbMB(p53WXQvlTf=QpzY^pF4PTt6^j=15&v@|PkD}~vj&|uf)jA^{n z=hYaB<0x9bXL+CAUNTtL=eA zp_cuDkY7Isl)twe4oBm^e3K;Hc088!@^u)2Nc-tjCcE)ql8E;)Sn}GhyFii|wOD59 z28@s#LZHJ1WYHNJr)yP>D!_R=+S7}D>2eU<0o1B{ELWOK;8y2}J0sJCf+#22)HP4D zJCzWQVAMsDVrK{)1cK+STG^ewHtDg#s;afl!`kSbZd0X|{toP7+iH?_8wpRE1SZ zP0%3oGiWa{ZB1pJa3Q(ou{728HYrFuxuv=-8>cseu+svsc}4pozkQ^AanMa(4W;q^ z*|{GY8a7&X4HLJ6S&Uz{gK4xIMuf%+&1PEUmt?F0xkS*giGQ8x@vVdBZvB$yiBU5fUUlqrgwe0%uSwmmm5w)M9MC0;|z%|?bBBeq)jUMk6Us@ zQ`k+Nfmwzdbt_6U&rJ?833=e0{qk&R?_PRuW!^<-!D*dq%6pgoL>^#n|Kon5`{2); z0+(R_tn8}%{4o4Nr*iZps(vhcj9rH#&q))igir+ZUP*)wiU4JkI+>&1zYw{wTWE_=5fVbKjTw*B4daiTac8vD8Dpj`tM4 z-spN32(}FR^3b2O^h(`+deuBN^l|8KbLjW|aN@8ZGgqPyZHZKVgFpOZi~cRxf5iXI z2CU8gnHN=iRb>8S;=uaP?`LAk2gn!CM91PiqS~BCT)D_%+|p!%iy<7P+klFKQcSM+ zVFEyEWUzM;7Bk-vae_zm?MlJ!?~jn_Yht%=%$C-%A4jOCNnEFl8GZ(I3Cw0KIHWQi zScu^W6OqjX@o|)shR2Q!G-=3_bZ(ISS{`Lq@=TyV)wL?7&J)I6;x@)8FYI;@Bx+Ah zu;Hi}Ouy(Kc|J?^^s6(*TNBJ<$tQ{QCa3*=8c!{>JeL%5eez>*k_u4Ail?cOoS4py z>f>kQ>SjVYmB+qXWITdAg9x`(LufWfe&GDR_^DBzX#%eBtr z0+&W*b?|Uj8SsviG(b`PkdJQg6uUI0(>_$?X(nFDCmy%6Qo?wN9Nu~?6pH|mpvq&K z(!M-He$`=zFkn&Y_Q_>!)fW=eXA2IZ9Wl$nQaPP%O9wwsS8$E3t>a+Rc6#u0E1+A` z(XY`XKvJwgnp)Fq#c6Y-1IW@FMTvRKP=-_Kz=@Zt0c~HqS&V6DE37qKU}F)p%AzM_ zB!NYH&EIlY!M*4SL$0TRdx%JSV|p-qB~mwjJ{4t5Z?Dd3xVTx1)5vCp!*nE*!-{%K zz-EPQ%aV>pk;jW$X`n1!NrH^0&#X^{Us%I>OKuS?=xHc+&k;T>JrhVXPElgYMb-9^ zFLrw_1f~5c%dle8w(y5klM~v{m#63MK1EZ+1EX7d>sUD4Z~~}NOQI>KUTb>PGmQ?| zK&l`mQ^wm#5Ad)K|W> z3P~SN#B6g`i&)EqLnK2JYc+CNI{l>63a0fgUL8C{H>fl`pyd#kU{r?0iSW*Qi;VVm zu-%HBgov1@CD;-l)02Juo@XAJ603H_@r&0MDB}39-cUNlJ|&Xg#FGCxURkQ6!tB~H{e1E$9++Y%W2Ik~-PBNi303E>fEVML7QEFI?$k6o zqF~g(hIwz1XhU~xZ$n0=0Mdh`j`PhPzA z=dxDmo^ziUbY;V0!OYUKGmO+&ZwD0J>5v9oU)sSuUgNnzeVLY?+f|rE*1=DR`#_xHPbDJHNH${X)vUOcaYXvBce09#NoG=|FwyM3 z*VyLuFVO1dZwlDq(RARjHL80XLB1sB9&q}J+-W~YU%}!_L@+Vq&_dOW@x`3`Dd0B} zoGZchQz3SH0NJ-0E$KD&J1AZ9$E^eZ4$U@+>Mu33=#FDnqN(3XAgdMDO&cJO9G*JH zUG-Q7BquJJ5Gr-Eu(XxlDo=AUH8tV{ps-QZqdH2%Kt??)$Yh)A0of`(MogYdJY+B} zRpI`Cyfl@wxR%Fzk}_QDN8y^Y%*g@bxq5+VhXAXQYt2zj7_obG*L`kIAjJC9=>r_ znwtJb8c`1ZPCXDzV^1y8Qp5LxK=TWO*K$f}nI!7=8t?7YPY~YkvsEyyVu|D90w=ZB z-}0M&;CBPD)wKo4%7MfLxHEhhx<_bQz=A>#Iz-Go*Dt^~6sEY5Xf5|NVlnY87__KO zWCsSa1YIW`hg=6DSO7_g1@kN|=_TfpNR}(heSOyTIA-OpO1REcP0CQ5m)7exQ0%-{ z{I#xbTNO7JpoiB6heLGN+Bv}==(v7Z{m7C~;}yfR4iMrK;vN;Wvo!X!4uI(h2~-Hp z#3u&l+Ela+uRlRmddQy<~wuGaf#4Rs#|Cyvp=4IRK zWYhEE{bd{E?n8C0tNGlHDy|gh1iYlUR0YzoA<5I~fdVgkUHAa;0hVco-U1?{5!xS= zhwo5fwW)ALSq&|)tCuuFI^z?7Gu(H|5eNNP>pXU{sn@F*E3bO@1q`1pre< zX10!G#sgS|75T;9a!coNyh4f!=w})`J|n6TJl5@S>tmRCJJg~bY@n6RpWz+EpDMYf z>Wh)%nwRLdl@XZ1;Y*X_Ba->qN4hL7)dV*e4J^1fk_7kwpZ)=h+v1^O&WcV$5pShs zRgOKk)z#WX4tyS+%prB!f66mB?{8a|ei(uy^-6@wki-qWxiD;8ck ziczqYy}}c&vMx-)Ex5-mXGtjEmPzA^$%FSP%N%FIf~Y596#{*t0l4BKRN{|$CEJb_ z5=t^Vn6f(Oxqi5nlwAe#^_AuyDwMA()q})meXmrFR){r{*9&3o?yzFqCj^t9|3hrO`J zFhOxkeRx5Nufq}1cJelrP>z$Jx7}tYlgoL2_KDkWp+L%C?RTD!!%78JjvoA7*QbqE z>y09gK0&9QMr|RpfnPpuy92%}gQ~y%za9?7y);kx{qF93wnQT5f7ONq2B6~iEE(LO z=Qxq#e_Y__;`H>%cQ#LPoQB;J>LoCm#B1(-3F_kGpg3ck^;fcA2z4w%ai&-Cq!cGv z79#+^bAZv?#_2! z`+rz_>%XY?HSBj7X%L4F1?dJUX@wz&kdp51Ryw4+o1weAQ$e~*S`q0+8U*GUU2E;V z*4k_D=bYC$|HAi&`+HsY{k}feni9`pN(MDBW{pan2rHDFNx~0_Qbe3~XFZ<%9Caf> zSt)ScTg9}bK$^gIXCwJ33)*H1)0ml}4=_^XS1+Myk||~Vs=nQ<#a4z*Z|GL0=f%!e zmJc%Hc6I=P<#tX8ZRvJy1o!TCp7to(PJV*wKlKu}D#sTVmhP68H}3A1RcFEiEz9d! z3(P8jp}+MKT7|dXGnT)4h-JmBg#Dz9IkAg%uRN>nCA_hU_RCN)4$ujWw86`Fp1FyG z7UK|JIb&t3sGiA`0I!_+p-_L@!gSPrHf!|{jaAJ1O~_0?;v3L-_K!b;+CuGnnmJ_Q z1@He*IrB{8XD^{ukqw9i-b)z1IA;BOFQMS&{^__d3cQz4jL_z1FJZ;mlq}D|*|dTr z%lVA*c-re46|;)xQqN1|2LjjqQWY&)s8-4=ZEB-iJDEro(L9wz7j+Tf>c` zGuO5Xi(bBYSAg5&-j3WsCa{k*sN1&Nd}P_SSAY8Q$5GRRBah>rM|Pfv18uF>!owId z?SOH~cQI$kih42Um~vUS7t1b3@XDFsXa8=jn(BF!hh-^mS+-(uizoCACF`U-x%pxiB6a`~f^9Xw`JxKq zLvCJmw_|@~c%)A4A1Y@I2I-4M-=9?k3(v&n!$xq(sE^q~AQ6%1zj_I4poeY2#BX>B=x-KoER+ZU=kK#}a+o>%+ocO8I zhs#*BShO0Ncy3^Tl@fITGcj%M`T~rC+mQO*8YV5DK<$_x^BZyvWmLtYDk&kQG1Dm`4A{EtQN}u8J%Gd zTRE4x{%0?tlV|C#Uc%RgjPPDUl{2*ZfAtcYKF`oCz>VsL_Yx}Ax|lx9wtqP_kvLmN ziSmkxY=a;BBW8gVz-B;&g`c!lnx>LAkx}P}UOl1@&BrjIP|rWEvX`Hm{-I~NvCP!i zpe6y8Wox-qU&+Fl`$b*$!wPdrnZ+sSMSadPlcn15;%w_hLv>_@wVBM)B5aZS%?k!* z6%2RH574vXrn`Z6t|Z#6(hwk~B8y7s()8@&S>phUt$m`0(u%YiMS4f2vjFt-f%|3K z*N0WE{qWWX=wROfw{nQh7eo8aw`o;? z=d-au<=h}V~aqoel9(WDreA1 zn!RtJ(O6VvK^aB5T19adu_6igwsGG1edU69AEaX_FE15BL-NMhVq-x)M-|< ztQ6|!QHq(w9H#c{Dx)J9_-T)6R|BfA2FPCyWG0rHh}G3s0qr?e2oKD~m0G7##_9`s zMt9^qZl<%b8j2-f?aC)^O&KTFDO$;lf+VHG`crg(mLmh zS5)TvYTxkjkNE+)#@ghL4+dE$rClTxID%3({;~qaleVYA&L>u;oKp))wnh$liG#Kt zT7D{(*cm!TwrPn)pHBluONLG7_WG}F{KM(IOB;B5yVGf{W_xC3fwDEcFL44M^t%CIA%MqCCFZ4}Qd&30 zZjvJ-*s1S_$xWk56gjL6g2XNh(#5lhSZ3tz$nv+if4G6IS>s5a5>XaB?iZoF$L5Tp zn=>edeulPSbIF)Uap^&#W$(FarG&K%)CPj56Q9C})_3T4Oc4jkw$I7$C~TfBcm%yT zPG$C_GS1%duFe#5b)ilPdN$us#h~)SnN1*6sA-+9N_9x`>?moxeUoSWdd%bgH2YD< zwxqm!s@N9_HQ7T^uCYqd2ORSFU7piDh!m)M4UFYk8;ZuyN;Mvuyou1jp;eo@j6A+~ zU(9Qhj>;Dt0{<95RNVd+WR067+WX-atL2s&jb>k^NKsi|Yhi z(9U~riWx#clCXFD0hQRToC8cOAE11?#&~2EW; z1}W}q@_Qa7itnoX2wM2X&p_(#GVH!kaUvUWygwKHVF4K7stfUdsxDD-6n2ss^%yE} z^aC(D9)nN*U<5r+;ePXp(QwQ&$v$Sj)BK?{W{1`pwD=;~EG}Qozjb9PY7)t zv-~{NQ1-8`tc5!B=~AzduNHpPz+G{1j)e8a-&7ZmrC+Dd0}QfsrwHn9GX!$ok>(ns zlCZ??iuuL^BmaC%&BB3Q^Y!8EpKZ`2HJ_)7Uzh3rD*F3t8?=C7z?Tn~wpeoQfAoa5 zyRE$%%M%)5?zmCho~^T5u~nAtR$TtsmGvw((&c1(x=e4p^Tjp8;o5NjE0@mm@r%>F zmGQr~LI17j529=QQKNV@2;K(08cZHU|1AXG2K{%{_5Y$L6fKruOCwd;OH((ALPZZf zU`5PmQ*y{aCl22fYMqp5l4jq>u$BIHb}+{UE#34VRF|}JYS>TJ6(hm;-&B_+yy)-W zs!JMntyW$?TbfHCu%KB$+j_CPSM}kf&-~9lq1=1>|D`81twh2q%<8aZ`&Sz@*062J zdb^B~sr%56Z@d?qWkJ}}Bj{Lu#eOLcK-+xFn8S^u-@qAx0U4AXOL zaeRl{v~xH-`$^{1mwjbK2amr$`OP@OB4)iK88K$rgy`cf$KO;J2G-JSIT~@s1Of0E z^W4VWJ@|PB1f`+Dku%!nV!^!VKii;1pG97NiB^J*Uam}Q1_iA6To$^lhTuB!iiAI% zX$D6V;_-pwA1Sv8Cu7)OZAQ6ZwQR-tSK6lJKUHouEi8(<*+t6u(z;t!Cl|9<_ffZP z|G{Kt+rb>APXK&g3~d}I5k|Lshf>MkejlM*xjh+^9KYU5!Lai>p9zlY*qnRw{_g9F z%cIWA^=P}!t8Key1f3uYmdWu8gM*h!_i`Houw|JFYsA zVT1%jk4O~14-~|-zM$u*E=pWE5hQq<^9W`)FZ!gmIs}53i|y+wMnfALBDb9LDMg$W z20jk^@$uV(x`AF6ckFk1Gn7RA^nD!8u@PomMnuyCea~C5Bkh!_o@~+gb719LtGeUr ze~I%!xi<-T=V^d*Pwyj)JQWx`0wu>Tfe`Z7#>OBTQ={+=N=iEhMD*lS;<`zJ9pmDl z`^K~{_y*;nwF$+TH1tvyr3|bleood@q$b3}yi+GhjUyY(P8!2HH?_%zp4q~vF3ibZ z$D!aSl$fFmVkT1)LBrOjTsaJ*bR>#lwA2)+C4;X_RO`~F&uDoou16`*zcW8ser?i- zDyw}zm2t36Be=yd?%p|_`JFn?1RxBP?GG4*c+Nbf$7qo|5?#F2#KmuabFELwPLQ9w(Jsuft|_xyHjH zeJYATYV&&U$=3v+S*dXke(tRAcp(Jyp-il_6r34d`}F+N#0bJDS=#l*wDlk)79#E_ zT%>&gLUYa0(Yku27(d}UB9=!mtf^>_xP=&T>4ePTMbo2m8~at9K=ufr8unLWS7A%_ z5lrg}BlQAQElVYFIj(@bNI#6%d)@TLJPnK+`Ljg)ikse3ZCArgU|+fl3~%fr-iNgE zD|>QRwTf2xG^MxX+b?yc3|fXWieb`Pn#2SIY8qwxiZW$iPQ}3JR~=;rv@%-2qpJE* z7A6bHAx_k_s(PW%aH!_^>;BP&qI650Cy%+&l)fec_}kD}H}TO3X%2`cS`9GAojwA| zYh0Lq=o5#zezYU44eDPqd8oxz6_fIf=AcL5EvE}Me z^{0pMcLj7JrEVjY$Wtj;zz}c-7*;ct*jEckktL)Zwx0))H3qSdnlxdh)nk+I7bK5v zSPC_FDQzx@$NyMw+-w+TP4IJ{e7yVi2E9-)aF^&nX2rS6qw~nQsWn>qDD|7`oCoqF z*PHzv3{h9S0IVxi8m(DsO!rA-TdQ2OS4JSAl|M>E8mrZj;3fHt#i89-5{om_g9?c;-pIg6lP25h1%Z|0gl8|_ zm>O{nkFr{65es$;+FfsIMb+aKTB{r7C9;}Veu92pHXZ1^-g`lSTE%6MVlb}pLG!_x zmORFHL`g8FOF``i6)nvOVC#p#qzV9A$4(e-%GW1z&dC(*7D=?~+4&na0I|a|AIpT< zqFI(QGt#~OJbq|3x6@`>n$lR+%tFGgXVbie~NkvwYSyr;V3y znLZUs1zxs0j32tbAuW+f+zreTRQY^4Q$onoDd!c9s77hE_CV2LsA$|VF|Ykb^OGrR z6RgV@x)`g=$_wMVY&Goke%tz{VDoZxd@l2e4E5#S&D@4OY^7CKGEf( zF&F!(vSqQFPlk(6Rb0 zzhu3rPN2U*uZmHFid=}SOtIn=$CK|HzBgv?R8s=aT>`avP^BZwEOH1X4YGxV7(;?5`-6fY z&wWBdB4MZ@V<;@|42Yj7h8~)Q3Q=&U6o)3yho&}%W^aeW^R~G}Vfn6Mc`0Fq#bE{W zVUsvW0G=0%R4<08_yWQU^93V^{OVy z1R+R@o%rO63c=a5q?we+AzFnZ+|N(n(!h4HOa3ufR`^2*RZa{|R}2~-cHK#g(R@tk zJmJm&C}V>yi-PwNIHtdbmo0_d{2SGkL#zgSRQYNwKM?00@4|89p+=JFHIsx}l1F}$SB{x=D#5{VQZlwau7k6qTk@(yJWfus zYYE}Yg=D?h1RuVX0F9I&x0I07lrVT#)ln*&1?xA zN95A1_0(+4#cZsbY!FEf4u1}wW)6XS4iPkm1WHKNl5t=T3Tw)N?PLKcQ%XhBZwKSv zx1<3#RY=t5~Z-P82m6kH%=>A4r0Lklez9~r zqbjiHFEUyy9G1#`3oUYQE%IC}^1dnZAu09?V>M-@(cv$KzpP-@m8%I;$_u+GjzKRv zBQ8m>wTb`_a`_1A6CmcetRwo)V9vTwz;tUqd(C(GCb>bSJ(xQ#sOc+=|m+v)_D>R$e+ zeY`SxJ*BN=b9gyHKj0RHmMvrA%oDDk)H+g*)(ynI3%V!)5T!JsFEJT^#RL4;(%AN zp9ek>I~08xZEHGO`5@Yc`7hs@smZCbh3KRfaG%J1Ix!pBoy5u|06r4zN`t*}?6|et z-JEVJc&K|Te1XyQE_vVu?XBNA_U2qkTgMHImoGRI@PME>#)nwZlpKuCE%w<5lK_#z z8{>dbM{Md9>erwrV{{W<%ofkkPR^QBp6TT(m>u6e%6X=36P#Q6`R^jMYA*cSja4R>ggHG-~JlQg5Q zO;YuvN@$aK-HlASEzt25c+I`=84|la_ zoZ1ZCj~?w;cXuc)vvmWXMbz{XtBu(A6K-Lg1OhF1tL&yO7AqU~a?5Im*G${?M|K#? zDo1&%(oct3F89lu1Yz10V`3;e_2ZI95%rU@oNgx$HM5Kj{pr$A8p_o5DjF)ZfPsopWh7W=%#WEuS=wHFTWYCaIT*~PvaSJU4iLbu6U!!HGOq{Wb3*b z@&K!8+U(RhzdRw1ws##U%Nj_X6i=8j+gU2sFo39ad)$R1%6zkfTV&h1ThJ7>yIb9s zX}Mp;WBX-^`w`a=E%1Q#18t`ow0@*#T>JJrm`dyPgsUvF?qu}Qf#B(snx6HUh}lZf z!A6aA5Wonv=ZEkrMhs$6ENb>(=;P5vZ#*S)f`-CsXEOJNtN04o{B2ssJ6Pp6vNr&i zT!xx$AE3Ts7r#gt`!Orp4lqeLHzuG*CsYsu5nc@`;Kd-T^e3m*Mirv(#)&(&kHFQp zkjrJ176+iC1^}sZ0Z72qBRw=%eMBz9(owfcuY}zJdKhI)iHB6Fmyq%2J!7+Hv=Y{# z3h5cRijo2h5;dU>uUGB3YcmqjzKS6Y1`r`;2g-^eyy1O}lm>|v=8&d9homeMs3Qi@ zi&1bWO>`5GqXrDCUtB%ZLo*S7ayHV#(7pahr&>;HivM@ZiX}>T#c7aZGPKqU3DGE#WdlS2MtG=r)YwQrEOOg+6T}!PBcU^eyO{a!W20ylrTR*T-bg=> z02Zp(BN=Pf$re%jQ*-0UOf^q=Dk1-&xwO2Cu1GBa{C+1 z9XVTf>Yck3fc&faRs`W1UzXE8ND8~}SQ@)Bc%+!T$tNzKaH;?WF!XsQhXSuw!%qTL~%W_B`ktVaY#m@sFKO8wv zy7-a?o>KQy(saXbS-YrIE7=lvjQ(u3PR;37%sc8}2K*oj#JCzdf02?@$J@r@k~(~e z#OeKBc9SjHZNwA{KdNAvSSVErrm=33#_ca?Ce*8;p(z5)Oe`=9x^WwK{CsM>ff*q1 zsC7c0zdmO*-CV+IdNMGmKKHTC7DUZ`D!Q{ipQmC+&Mb5^V<7;UvIn0|=S0d{8{G!%qvPHrzT}kNkWHOGFRh>}HP4@3{yl9rSwTX1-C5yCSy3+GOo!VWjW* zE2mdBfy@vQlosxi8XJ(!qn;(2;l}2+7q+%(ZN@#7jRuss4AAuaIee+mc0Lii=$W>m ztDw1#bDJaYtv1Em`=-{X4~{ZJior`*F5T~S4g;BPmoe#@dMNJggO}{T5ujZ5qF_O( z8h)$+9<_)slG+E2EUnPM2wUr9c{d8yXjuKIZEY}43zrN`Z#KD?v3U+$td?QY*( zzHM=C?4q&l?!Z2}ZFx)XvUTb1&~?1+%jMW*=a0K1pGWOqQRJ_>AAfnKFWCO2qr|mq zxbS;Sbo)A8^qcLg^pljoZ~Jei((ccS&X$9^lv-I9mhEWz%t#^2w=SAX|EU@!nLPG7&%10tUX z#y%%1KB&z;*+o7vJl-}azL@ZiW>hCS5gB}S%1|CqM3Elxl`qb!4>_-&pyEqvS3i^! z5YB)zmN}^?rJo>=Cq1t}m%2Z~e8z`4By7Q3@JUF%9!TO^7E63gO;aAmh9}wxq-3WCvq)2a|aZEsn?T z4pw*umR;PzRT9C)ror8t)~}j_JqP4aQHVGOKq^H1YU&|6<{{cCA>%0iUd8VaVCQd3 zdqPUpyc7xLBV9uYCP`94NZ!MH7juZ?wnHPa!v?>Tb2|j2^^4~=hn3HVRbGWv6NT6E zhS#f8W~hgIgUNQLy?jH$JLdzEc)jwk!sDCdmU$w2=HWZIhyn_)5p%-fs|bH^=#)C# zYer(yKm8gU@m4Yt8(U$mKXNEX`t_VuM+&Io7?*A}l3yhJow&%TEU@x(;>lTo@xu^Ysoe2=zn3|A_Op_n94Dga%i zc;$-41b2lrxmuF(#Zha-(YnRar^Ye1#4#_#v0lfq5yx}z#dB%I!#kRJVX5)_E%Aa< zZUhVQFKSGM`4T!5?Zw;@AgKwC!7Gl8uBF>!+ob)l6h*9WlIt_ zU4r#>lF;{fdtz*PzGP=Kd|B6I_m*VOg=FvRWXY!32M9twZnz1nF@gU#-IZR(QqvS#(%P@nJ`ksO^QHIv z58sJKaEKl0P8`!}n(+0~6%DMT027a`MM*QFOI8WZ4Bkpp=#QF{asj#F?_Ot~X=Ls5 zWWk{$xbM7O$h!ZN??iUbMuldhF9u(=WHT>hyQAbtZe?PXCgEu25LtQ?4h21i=BRaL zhA-rh@#j)&=F+<7(nE6@TXUu9a+z;(*+}v@`180l^SF1tQsBK$7OCX!|J8TO-Q+8f z6e#f*C~FqP6Pu|*3nJ(8G#3jnAUQM^nc6k4oI*j140vWx{}3Mv$sCFHdR(KJWK@Jg zM2c7-LSaa#*TM#5$YS}}P5A^##T0+VjAq5Gdxha;nOi@y zcuvKEd5Tb2SrcK_BXDM-B4`sz*fSLWCBJ08)nmB?mo*G`fuh_#wlpWX(9u$c3p>t^ zy-K^L>b$>HK_sS?(dqX);sd}hdW#-F`@ipqhGK~SzuggYfPBrL^ymEa(1J*LRrNIx z%%&Z6_7Yjtn+#Xu60MS0HQOwgcD>qoY*$*|?ICvx@Rt4C&-KSp$z;m5)NTx?a9aKD z=S1lQty12;rp~K_68yX)mhW>!$3dpd4?dydYFFH~!u=d-i@LR>@)dYST;s))2k90u zn-h65(Z!zRj-@0B)XXXxKSa?8I^Mp}I%ToJ^truaCs3PFTuS zV!|?B@WSaW>7?W!@5z*jz<3+%wJ5<|7eiagmTd)`7X%i@5frRQU%(G>#MWaKECc5f zbnobi^wj;#3nEPud8x#i(LtsusF7|HZXyyTn<`H$`ChwQkQD@`y~XOC%y@xRuodb_ z_Lfpg3w?qv(iQ#8EEAK|vN+3)QByhFmw<6wI0{{ZGD-nSbE^>BYlA^w1M9p%E31-c zH!;0XGc)y3B7|1dAq>_6DvO+y20@=QAo@VsO5t}z{EVTE>JJ1~%)^REf@g)5a!u%~$#m1a6mG9OZv3Yp!90?RD62V}%g?S<^+>aBmoQSDCGRC- zUifnbhjpteFCwqLMerP6tv#Yr=UvA)ki8ZO)ALr|gt}DDtY^aMEn{ZCmp40*#IT#) zavn4Hz3P6}AN%!lwm%M=ewqG9pIQoF{?Dn{a(C+s80A1DX$q3KU?Wyt`EUzyO zYzb#)6>8CUM{n(f_*cGMelfnvwW4OdE?U#3z1eR%y8m(9{|t6}HfIOByV|OP-QQjw z!IcDNHUI_k0Z4M}gDRU1BpmpF%zx|)8qsm0q<_943u4g8({I-;>S~xQ^Vf~f+~P$Q zNl82opqDjJ1r&)SH8w>WB387~4})GE)W26!mc<$41&N z6Qs7*_V6x#k8)L}6#J0eZxfjp?c+H_{O%GQIMPSSlW#D+yWPgk;7B=wmQRhQ1i5|@ zM^2cY-(FcXNY>?G`ZnSV5QPDt12g=Tv#xe%L#!;Q0QLHP_1yB;#7_d%c zx=+ojk6keYkWFL*;q;ao*Wc+awN)yJ)EUiLBH0RO%v^ z^Yc&ILJmB#6xe5lo`6Xd&W zCQjRIo^9)&m0DSXXO;Cd6J<4JN@9v$GUH^IVv8&fJ@v*B&rCs>zvoN%&P@6PbUW#6(F(KGk7v04Sr?)g~yc+9^ zUsYJ{xGa8ZB}lCLMQ;gD!YJb#n{}pz-jos7o;VA(-Bs9ACCrDFJ2elyvTd*~Ti%oo zt{aQ2d|M>*c~|qYb$+zcxoPf@4DS=A?f23K0W#w6kuy;jah~8`aa?tQqN)SvWLMF(uDXfFs)Kk&4CZ*% zJ3AH`f`yh>ani1O=;dm{)JDD$NX`gcIn_i^Xpth0tM{6;)kH14T}CCGr+@IJCdMpX z_c+eALlW68c8heq>v~9itTw4>WP{b?dRQN;E~Q_vo!TyXC zlYnbka=|FEe%@UMhxHV#*8lz3Bvty#Bs z{GiSufbOl?o(M^IBH2;F!@tm5N@+JU#c~a$YNM7lUBphkEa=&=f=-S16w0!DJPt^M~kI%;+s z0qYBisGc6(Hu2nmce2&Uia)&jOd*AYGkcU{TDq5}nXMHKN8dICTECSz&D>P9y{k(8 zIxXT__FmOa_Sx-fo{g*D3X{nPk@i-c_sxAgW8b4ZZr8~BbDE1T!T!Ow>m9Jkqk+WB z*9m2q^TimkgAHW|pTu((S-*ziQ zOrY=T58FVCyOkq3^9+{^%MM?O>1eb#Posj&=fYHy0y-I%-~A{@gR$OUVS5Z1nH@{J z$ljm)r=KIPojt>t(vgaFesbZ&8R)gT;nAM+LLPyCSdCY(NHy9);?XwmK{7GhoQD!6 zPZ=lQm9ZNxD*h@8)d~+belbU@B-zuDr!>5(AZ$M>4!>Xof0Y_PP@T&Iusf!KPn?0j zWVb&-tZPu7o2Lq1=Y}8Vq^FWWfUSr>qJKbXUqH=s0%6zJ3fmli2Ehmcynx5RD+B;4 z41k3C1X7*d<&TQXs69}f^D!7iC7lkb$?ZV|)~hf7#P6fYMgNW83-Xy3{DI$ZLf~nY zZ+OjbHVV`KwUEbV&t0a>CJ<4A`~iY77H~Z;#{Ufj!v`3b{{q2ue#7tIBt^=&Y-l4$ z?f$~=e=g+TwWopdru>4Ds*vK;Yu5VNJvb8J_&r>}5pv5x)EU-%(~ttjWLxXu3wbyQ zmN9#}Ic}^mocXGaan-MhMyc}WLOvnyx&QquI0zPocDVU+ti9z;sIdeCrfm#=_r`dg z)sYNu7bHypn|LTaH-P<9_MN>n#3*+?ql z>)lA!4`MLY#*VO{jWa8BTTZlW)F4cor`g#|Z?BQs$|xbFrOWi>H#N)hQJvNJ1HW&b z%L?Oeoz0B;e+q*2!5GUzrP=xSDu5Oo6TZzCj0#~7oM>V^I|-~}2%eC5Fo8axlV?S` zqLuB}&vb`nzWoFjaP_FNEeGYO^~o_6D@i{=FcNeNIDWr(*mm4k_RZ>-fV+Z&V4b%Y zZ~+HEVLtvT;F6k<>B^6P6L4O&R6ac*x-feKd^$!J@PsL5p#<4S^g!XkAnTJsdNu78 zu?oQ%has;d%+nFBIdQpB-mP-Zp}oVO`29y2+X;;e+5g1v)oji`shd@t&uaI(I?Pc7 znR3k=C)+eaMYD8P=PjEKs&Q-|T{$hk6=3ld1yfU>iZHFfw440eY0pkp?y&bjOsyne zm{)r%FyopdR{EXS6CPwZZ}frDANNfg9A5v#?{BvAlWlKyii;}Y0y#zy8_}tu1ctlvCfJKa%JMb4T zJB6zd(t1y)k>u!3al^DDIJ;l=Ob9$?bW})S!szjGYpH5n=x)dA>Fq6wFi=j+=Tt@y z0oOdPtt8(ag+2Ptb2?i|3gSV8{ZyTUp6DS*fd4t?U|_an!QO$)o1DkmsA?R|XoePn zIv)oPQRB)9q-oH|-Wo_bn&Xh|+6!O*FwlWfqjDtF7lnv$4H^u9nhj{!Up@dENaIY~RF=mu>$X$S9TmB8ZgV}X;UE}}B^?-1R@X3_M|V;{eYN{L?}$=L;>P*OJ*fvQ6gT)!*%@8S|a1(rxF1sKBH zys2uML1&m)E=aM|;@qi%fb1!zs5%Id_$LTT;)WG(35{6sT2mBraAM-((^zCQ!UGzx zP$pWzZvyT#bt0mWt4M0pQu7o#KT^ouG&pMOewy|bvxv9fqMSGGG<}qUf`5`SN3rrW z;}~<|TQv>#vtCZdi*~bVg%a4XqErpV=8PF+hGD{wgiH2Nuvml;UI4^@mh&5auh>Zq zU&xOZOYjU$M!BEmkv}YfNJ>w|LC^BjTj`~-fg|x#oaz`B3&OgsQ$enhFJaUW0mIkR z{%-@-L3rz;Zv&^q#|i|W(G&tq7hWTr)smN!2d^qG%;?jA8ORcnl&nZ*Hol4p+3asg zNpcb#&=Ai8zv8x7Dg$>>Rx>kkwH39_Enm!6N#16Q~ZhF zH&#}Gzgof27ZzZP7mdxM6}B$JOJ8m-nm%+aTKdZ@XAU(s+c(G=a)xDaqjU2P_nPCR z?=5JMHC1*i36rByPREK}X7U{joTre;izy{iav`B9b}W=1cAs^C_-rJnJjOBy)X23O z(9Dlg_1e25DGNwhH2ocQeJ`w=pBkGRA(qD-QC|M1fLq1#xcW$5X<>uQH-*jf%$r*L zSn^5wD0K}HRCb5hpYP$^}zd#4+XxEe}g-n*+^9^`mj;> z^pIZw$3&kdTrW-C-IsjztSvzHDH)YM&pPQ`=rnKAvO*IDJ*E^;;-DtewI32U!CsA` zRG7`W6zUm7t$^u5k+ShNn8-MK0zrcrEM4yoT_~Ev6E^xp2dj zNsDv}Rx=8)`hDzYCmI|`@W9uGzUy>mU2;Y`W{DB3Q3yG8R?(%*{>Kc^+%W6u>&r@E zBJG%DeiVj~6wR z9ja#llcVI&ijm_}_ysE?(Gt0SDpAV87Nr6sWt#q-N`%XzJs|2&PNB*4 z(Qll>Z+k5_n{PuYoC1f8>u{#la3+iMOpE3qSk)PcE4$JA-A!{GD&wzi>4^Z|V==ar6CLG8F^^l9+D*7Lr-ryrPQ)*VN5#TS$Yxzc8&#{@40^{ijUuANTkAt+W$ zO5%Qox47DHu?lw-_)yyD5tzChQJPNo;O8wNR_wSz(v_05IE{m#egwq;(beNtxJHRU(~} z+ZUYYhoCVa_~Bn=(Le09@+6#(f#Mu}>m*)eE^5MJa-P66A1=Xt97=glaQI`q+4KKQ zCEowjUd!wI&4tIsx>5f<@?ZB_g@JE7PO~1+LAua2kNt>7vXOZPy0F}j{mJogzwTb7 zV>62RQOHtMh?(KwHWrbmzFw`R{vPY@DdxufF-M#m1$%1{zSmlg$AJ0&zSok?#YrCM zq2>Q=uT=!;Wz_s0raqEO&@_*Y$B5`hjGHd1JsageF+r~L<|$D}AJ)?(EZbS!5CA8U z&^FQpWjo7BquQ;KU0Z=0}}ivu`vaNssZ^sHcTn@GX_V$;T8-|N)Mfe)^u6gJk2~-h z3znx@rv*^C=x|;@3Qj3v)kBH+YO8{4dvkT6Yoh86JQ)+XlwJ?WWnzS-gQ@Z4a`(~b zc_`(Jj4iSwp~P+Ej!LmHhuIhlW@ZGAQ$Z5OxsuQlkvRQ2s48^<{dWsZ!z(#FgZdJM z$WnMJ@l&bh`M*+$+|1d!NLWgod?3Y_pPa4{n8YP<86= zzqR7Ff0MAa`;@4#y9oVY$(Z=&d59kS1H4GnuxcFNa-)<*_Uy`l+R<-Ty9A{f?pN$v)39!>#ZftrYy0G22yX z^wSDA9xOG#+50k@W)B?c@W*)i8=E!=3{7uuIo#(H-BcL#yZ+AkKPXYz@9$( z%yoSV&qVQ{Yvo*@A9FTE|GH&R_3|Y6y??*Vevd@|)d!KloZKjf^lzD{|1$IcnneHm z%xAUn!;WD)QHzNc>kKiDV;G|YFv&_Vz?0}CDPM&FsMJ!4REGUD(Ex9T%@p)b$<0*r z!jfM}^#3;Vq0m&nzb4UR774`j*Ir5Q6r?noSQMr^w@zDT1~Kjy=O_I&69u=z|6}Gq z!wK83ZDJhWuTu;{JE-?ym9nwP6U*;y2<00-X!;t|79an>RQFS_(K(voBkF*$Mt5i5~A<2jG(YWrcNA!I)MD=X0$0 z4=(17mv#zdI4;vRATQMr8515J$WFvCy0G|sCR2HV%|}uIvyNT9ZG553gpH=lEoZ?b zcDag9Y^(SU=V9m}xXhDd!lf7Py&!ewD z9(c6paYA7;c6&M@`Rwj&M$PW-eBP|;?jMrqzh=I7t1AW~?@c%r;^hz2hIsE|Z;Y=d z->+~11Hh0N1b8M2HGNSz+`X4j@hA~j}=gc4IWp)1^-WQr973I?Ln2{Q7 zDOVrENlhrL=h0i!8ORg6%f3i`v&cv$Ets-W_zGVPgCSzxuO#~4XMR34oz$STCI{cJ z2Pzq2)U(YQOSE#$oKNknXdF|Vm0oAfW8wf}m{z$|17mKrf6 zIZYXmh2Jt5C=}6+ZBCd_>GhhVOhqCSW~oWKd$y!Mq;kNmFkL_)!x;RQk-jYppZSAh z&aH4O98n~=B{lB8SjYIOz3Am-35?hg`Y5Rm!$*8#L-zeZt%A<*y4a%Whg{ET@}+## z;wjg-j8O%0yo@9voo~{?Vl(90U&W;RD14$UnGq&Nq&5eN%Mx~}(MrXnUZUj!5R8?n zW9?CoY#FiR4@;s%E$KzHofT5aeT%5Ci>1RDvGXg@vr*m5Wy~3HOVJV2fH9>2Dg1x} z>%CmFNlOv*UHV#_xDu2F^VcIo`0O~D#gv=2&m(zDX(@MlByC`r5GOG7xuB&RE<@o z4NM%+7oS=*pz27fVMe1?v`+&SN7?wqsjM5snk1#(oHt((vpS|ZbHET00TkDWNQggg z;Ew@309@epfBA2q07Kv_)ZTDxZ2iA^1$^J^CuqjGQjrf-2;R2T7XFE8MzfGY_?>Cy zR%Uoz@;bge^H-KuC77$~Z&}(Jw((N@6#ZW>n7F@UYO2(!d_}=M+7@#(+HPZojJ4O4 zC&PlAx2!WVPl1yb{rKk%+)=Le0lN`FGhf-pc(&+A7UsCKu8v&z3bn(D^XCgDzx_9Q zp)Xc`XPW)zoLT=65fBZMTe#C%*qt*7AZkR_i+IpU|8s>pPZr64vAq^8jLfhe^V$oNFDy!0vK}YPy$~NO zC*k&)2>6PAK7v-Xm9%FX5}MT-*pXF020a-w5ku^a-tZ4m11BE5M($rht0|rsC>8+OqxH zk*F)vI*-BzrurGxvZuAPH`msUZgRi=8%&ipOHpM-O`C;fl}^pl>xZqrNf=eB+hgz> z_@qI&T|)joe1-Zo=Te~!FkNETrN`*QmI8eCWV;w^&9xegC<*^>Se|<*A`)4ZIP^Qr zVw?<6C;wcb7GWl|GdAs?3|GtjTA}{?1(Wi;k(WXPjuRMRKUb(2fVWe4-+JHLOXT65 zPrqk*)!_SD?*(~}=2Ef~SkI+Hi~6wxi?j2UjrsY)aimgA1#;l%g>}V92l;RadqR_l zJuYm$@iY8^MzpU>Uh^)JaUj;GcwsKdypp;vSwa1g&v*{LCQ8z~ZBlm!@NH!irk^k7 z{C$P`+kc}i+i^vAX(Ay`SH&Ai96`_w_)tFPJ3fWpEqI8@h7izVH26a!Vb41oJZd(6 zdolk;DE_kFA9i^8Ia&T*d@=25=gsUCMyMW48UlL*eBw)cC#Lyju+;C&jzKEN?l zS42IC5~?B#&PpRg-xGOapeQB9C?V#nRCU=HhHMbkXo* z%kY#-;bs*RaG;WhffiQPaOgxBxdT*?5ON8pjWUBGzq4koK?r?oyPjvpQVLLy7;Eg@SHSEhIVvm`TqM}q;F95T0JQwlIr}kPC$mDg5%Whf6 zHBcH162pnlo*Y0C!U<^hRRVD2he*6k1);G~ho*SG4n&U|s?k;;cVePO&$$Gt`k5pY z;e`^&=MD$ve~;<9MI}3$$5xlf4@8_y;WXPymLICd*QbUU5J6(Bl`ySQDYQ76rV^B1 zQUkeeCOE>;Ag`iM!JVGJ|2IxEuSVeijX~JB_swY*;9;>aiZpzMdX|m)YTMhA7&nlA zMjQ>iDO%CZ9x8`Neb~OO$R7%_v#h7vzb#hEb&-jkMGPk@zyl6^oDg<9BX0l=a1rB= zi57u6vRBbtNm(>-LyHBph}#)?z(gCY~PDWm14Jj3u8Op^Fw?)Hs4+|}zP zxa)J_H*m$4xFJg8B;<1%iw9|hGcfc9PSjLY2$P|faPApnN5;DKf~J~Z{ct^^BDHAq z_3KT79vee#7KLR-C8~!_6h5D;v|i@!kg6&bed^g4321%r^9FvcRFa@p z^ zqZT?hvdP_c4I8zOs!QLJ{Xcx21zVJH7v^E8p_!q(yOA!50fv+==>`D-X#t4=hVJg} z?i6t7PAR1&1wl$e)^~fa-D^L@^MB5{<980lH~!4{&a~v;O&v;qLX~F`K23<4uzpRs zBpLqW1UpIWC;@$a4*jNFqz-7F=4;fV8gc!msrG*nOk7m7bJqQ%r2pfApXm2IEH}n` z$xw2i8gF#J$rt-2RR_scqSgPZQ`9you7=RY;AO@6Mx{XK3^wO&;00RiWnmT{*vpp= z(XlsNT>Y>}I&;FYCPB)Gi4){~(?d6buH4Dl>>AQ4F-i0%C(&qwz~1|DlUKr~n>zV8 zQSE2hYB_WOt&#kFbjqq|3n)J~5Tvp<%Lws4?IaiY-lz955J-83xeZLG3X3warT+Y!3o8W8GBYyrKTOeJ*j1}Z>;twv zA6euG9VYsiNELyToM7MDO4n##YTz0%!>-RORLmOG@qTgvqFw`y3o-J09YK7b6T>_3Cgf5LwqLAaxM>I4G=Us*_JM*drw zE38HFzYjhV>U7FT;Di6VFiRa9?p87DicEg^sEAtrACZ*T<4$b@GMxh{aEn2@eZP~k z#W`$cCZJV%(y!kC`K4>8O<_V;d5JVR;DtWiRK2$K+C+2RQScw>R^K<%+*;DcLIvLn z5ur**u|%buQ%J<1s4Qp*7DG$RGztQ+(a%T74H_nWtaY%Wf2S_2F=5hJ{p(re}|ASEbS*4twGD3G1u%U$)B2Rn3bhC zm@#wM2FWU9Ec=g_oK0N?2Tib2i5%p7Jcx?@If&PbHL>&Abu>P*pB#BwXG5aXRW!(0 z)a<%T_tj;|u9>5;pvqb$v@ZN?dg4yrmHB_VUdT$63pApa6Wik}ilJ@mf*Y0S%M3`V z0NFK8r8CV^8*W3aws*2Wfr#)@Fuh7vn^FDJq4Kmg#I{3bE%~%_|JYFvTv=2NYQuEl zBnYMLdIYzlJJMT2r6yEKdjyZ)63O{osk&NJFi%jny7z_-ac5X3`qVRHSnx=M7OvO0hWRv!js82AlqC|{a-Jz-FDczbn-Wn7y;vpUMi7o!8{3w3T_ zep(ZfPB5l%y#$%3g|0_MI^OzuSlw?6Aey$sd7>ta3U%j1TXcw|-Zh+!Xb0QccZi}} zGeXdr9^2kQp5&Y$QfkT)^Tm=3LTS7osffCZ`!D>*Ebb0hfmsoj-6J-t_@ea%cNC5~ zz#J;!4i>RcGFmyoBsZ-v2f^I7%CQlytHx-9++xZmG9^)`%Q)SsEpnq*edUG(AG|iL zDgL3`zT7k=!Ua{{kodid=L35sZjLePkt)Zjc_gPhGck)~jYF*j7&De^SVjIQ$HJPE z3zuxznf;`7yOUFjV-GlA*i`>Tx6<0Dn7Q9)$;#XcB$2M!@`U&i%CgwR(#n=ArLKYO zJP=uwA?1ptSs*7XM7C*e2^C6}y2Nf%j>~?z=mk{CL!TfcUd-lE{2#o(+s4XrJ|!*j z2vLPI_hD#?Rs8PHi>jH~uY!J52kx7-H>OuQg@17>PRoSZiZ=Fu%Cu=`*kVQ+ zBomQ}sV2s;s7vCR3R2*J#Bpeqku0ZndY zZF}V<#_GITBG>BO9O;SgUGw_cJ)u|J`XR9=6CA^dWa8CgnjSdroiIX*bUodFJ^ViK zVsh*0*5~tIT)tq8DcWa}`_C98ZlRd51Sle8gZTPxF!CoJpn>5KY5A{@T)mb!KAF7s zJ3NbW#>|z)*HxHJS&F(;AQByfBA+Y2GGkT&%}wcuSe|>ldF-nMHy4I~an{7hO>%yq z0)pE8QX#Q$zgnYIemV-FGXXc|kPjJ?pGTB8s%6#s@u0kMe-A~NfO%OV69Cy)I%AS{ z^_B3K3E4j!X#^=GqP$u1vwA!pk&hb(av6PrGao$v)Y=~kkj{nUT^Gf1Ztn4{&qlFa z7g9?(#>n{3rU~;DzpC3%vi4srTxqS8cs|tnBu;O{_7##l=GL$m~OR31@f zO@HrA6(7Spp!dEJz(GLC*;63axhqOd_PuiLE&n{-AI6^uA@7R18K-|Nz5WuKwub4S z&Wj>_>m$OQpYte-HI|Bh%pA#o4RnnymtYwEE~GSY)BNgza}^x5*vGw~!$PXgP)Bhq z7HAAIku9Vvy^L|BXuU`Po-%S}V**N1c_@WQM%EbMJ}42bZ$|p&AX6fr;=1R?9;rvmx{e!?R1p+D%DD@?C(M)eK}>+MbGy1ZmdX z!@F=lcT~#EPT?$8J!{qIg6+@8SC>@2Lmz+l!6g4ESh+G9l^}kuYY0GpaE6z>d1O#b zhX$k4o^| zL^x~V) zjL9}g)BlSGc$m?fafAIj$M(~wJ}j$3HogKoinTcn#eFgoR&HeNVy3&VP73aBbVca+ zNq%IqtN>6T4J*4d3rN@_=&V)a;~?}7m_f$1z)Lq^#X)Har0-!oEcAwdCrE|`oUdnP z&Cm3=+~Ez0sR|UVSNVa}Jv`mcm!&BDqk;Mz-`l?dPlW2!jbyTun zUkZY3|K1nes^fa27x~@W@QUP>GC7r29l(r95b%s}I-dXN`?4C4cn?qL zB>i{pK$=kBP?%DfnLrrKXFbn}ugzv#^y=%pP)?_H`WO8l#Z0r7jH{Mr>^B6UIeNXZ zP*Tg7w@|X^!|++u2%->yJPfe&0wFk<4yfhK<_qpAjvNP)b+VC0lS<!f^vyWT*#{%BQ{#*-7JG{R+h9d(;tbW%8%<95lsJdPl3 zB8FcgebDEz_>w(AMQBG&gx-V+VWsA{0w3A$lmhfPTPmIM?4@mn3FQxBULCw@=_C|y zNeNL1?<>_dij3P?jU*;#E@=0(A>nRb;HaIWVq8FXaRh76VWBKAvS(!kXPGaEN}p+E zhAD@_9W!C%((9R-kyV-TU75+`Sy5}5n5}X9DVA!6#9Bzs<*XsI@)0g%nfM``U)v&O z%UmBtH8d=R%aODEQJJEM5V(0v(emHKaERh!eYzN$v(4#KRXH!lY>{R z>`PWf2@iAGiA3hbNMuUnms?0=Qu3Cv#JijFXms-&l#?r8){?eXl2-ll-3=(Du9%^} z0?o)$OV=zKpimNBvlozyuE=Y6B*-T-^jS8cm%I>QthU`M3WR<7l~o9|$#N2t&iDhu zfMwx}7ma)<#G?@XRR!V+V_VA1M>$c&+Dk18OK`r7>$`Mb@sk6L7pN@JPj}@g_&Sxc zIHIwa2zTcQIyuo&klh}Mk)`DFGzCT9m5|SuD1=1G@QDVdl+ygL;1x}N-Q}|97kbK8 zR%u@*?N=tzT^7WYU$X{h@dhK3;>e8*b7U0)N6VbL%g4zy5FiO(%TQYVOq>(AUUadu zii{tHxn_59@hyS#T}8!+J?40VVs{0+nhJtNEccQPj_FoN8PwZIu}oAh$|lzoGRmwT zC*wMHjp?kiL9Uj`tTaF_j4a_H{!sZ7>So1}&m(V$VnjAd^1hV?DTfbTI~eH-MAti! zz=cIsu2MZDUfbadvRYwA#^B{kh7TGREzQFaScD7|bpzz4f81>o>5wF#+S1l^vq>aD z6DF+8iQ5!4LL3yKq#6rl^;?MSgQfaY`(G8K2&OcP@XqsZ;i_OU& z2#OQU=)ok}Zd}GPjCYC@&?*usevWfwb{J*=NsS0;4huPDHrYlK#c31OV-o{q^R8am zU;k#W%Lp3|fh0P+DwdFDuzR*P2pzza{6d#Tn9L=4$1XELt{GkCqf(~z&;sFX{f81! zeA}wi-D*(Xsvgtq7ez;H*l0z>z*EeF-vgq~q=Gl=2;35wVYi!0_*=bgxB1wP4r+I( zv38VbcX`+TzNbC+sNMN(#|P{V?;L-Rnhy7k4!@fApqOmUQ<*9X)1+U`fJ7&o@>d6} zNN?=|uD=HRdN6eT$psW56MuZ@U?HB6?4)dop@@cZ!1+3E`Y!N-bzcjGTr7AprWIR-w9d zQ0?cpeZ7AOu>okKZwU<08INQcy(DsK@gel)-D~Vnn$aV&8+tVK zP|gj2Aqxk*Q9O0fvWqpsm2x>f^L{{rqpuwFq1u>gz+s4|ww%>If&?URCR@ylqjvrA z%Xe8`dL7f1<1ezP1JbFVi^5INyBx0I>esqf%#~llAo}+o{a9k&(GUlKQ{x}U+kkp< z9{)}UJk^ArBCMtaWs7MzxdPB7N3K}ep#vkz+ppd~5iDu56&a4^>*f}74Edi`eXP|A zJd>4huTdWFNRua6%E|t_=*jX@HB!50X1@uZ{n2V9J}x`ini|_mJI|$r^Y#EMvpdVN z{;K!@^kM8(($1WP4?>K*{%~5W_+1aqh}{n zENvgByPlf3^pRpDFlxjD|Ei-~VaJ22q0Y0n>kCR0*&!yQ9u@O8C1*Bdr`l>&n2(F* zZY3t>37kSROJ&e~nwzrzn(*0liZWhm z@$ZWRI-(8B^swUQcTew zI#gAYtN-CySN^2Z*JY8vlFvGkHm{gbD|o3GXM_bbS540xYTA+Pzk>V_7>UwV;y6#3 zv@le`Z1ZfBI=y=H!RvcCfv~{8e@6)EQ4g_)Gt#0Nc1gfB8mc>2VOqFY+5{oKN4CWA zayB*fRoG0JxToC$J3p3#CN708?xqe|e>vDaWNnu6vKYf}BZ4uF#yuA13@Ymg#i#@O zND2A5$Ti?-8)p7|W4+(6qP?uTzGO#t__&xhb)}>d|jJrZ(ojg zM38}8DTFL(ZNL)7IJikv-&IUFAszI3D{rpXCwTx_TZO2&A)S~>n*Hu^zw3%hkfDdI z^T5pP$Hri7KGipxz%PcbKTO*9UoLmy)te`0KO}bX?U=weu2e|pAs;hWq%>-^Tn}Mp zM-hLAIA5d_JQsz-TlP>+jv}S`+`dz?kNfOObs|w8lQh$H^lTDrvK*C_-cSx_o1M5e z9cI~`RG*VG-W-vN&$#rT+(pTx%!qf#A(l=mu9!|=npWAq9y%(^5kE_c@hp~Bo4=nv zo%l=c(SJ4*PptJ%)a3TAblr)Lo#QY(9l|)CjVEhMnqPO3Kr-omKAZlAN1b0qw3$y4 zSTDAHgE*mJs+RPLnL$Eogd@R?=t_6a1$Be=0cht!r}s>DK4C`f5m!; zzpw{hIiQ?GLKGw$!0G+M+w?%Y^)pb zHh($n?EG64z>ai7|5nxcYM9=G=?&)wd<&WOs9rcT8niwx5M3e@pWv5?JVB!6LL;0BjO)B+7x0dQn%@J?tUgnB z@Yt9^BR?NeSr?)$h*3GtIx%~H1eyzcOLYCY>8hWQJ4v@R=E zugjWL7N#3MKHRmxei9t~h4}PjNdMH%uuZ3aH};vrBaC3={gd}=U!jYdA*?W@KTpgr zW@9w5(4W#mxet#?ePy@D6Pqtj69s?IUKXxuD$8YW%io8~bvflh)b0-JD9V^>p#(^GEy-w` zXLVBU4`Cu*C>9N7e*+$F_L{+!#}a>G!5=oh#x;-rO}S9l)EE? z=@RHMXc2t!xI&1AFZx%imDC~kAIICH(Kj1;h$>PXhl$K#TCAkjrPcgN=?@x6?5J>t zC~mgmctl8cN#aO-D<2zHB>J9)9Ag12xq`eTwOTIBn`umldo{_TkQxN!jmjQ+1y|$v zhBd7cno6ZS{%Q-B!pL0WpfX)M73;3R?(sBEr$1I3p2mMpF-#`_oS1)+O4<$JXvi)m zL+OPjY6`H3`1Q7SV3QwQEANUkzth2NWI9p`fhI8kLROP>m5`bEn;=HAUQWD*7okE} z5`L0UIb=sPVMTQfYB<8QLSPw6i5reDf_yKDUnGZ|C$GZ$G%eQ70pG^BuY(U5KnL=!rEfv;vR8rB-`x3G#U7lTnRitnvD)3=3 zgU%&}3o3KfIOAWmF5^+h!ldX#z!j#U5B%ABk#D?%(L~e5&P@M<;{6~7s|j2FvCRwT`tO zI=y`;I_E7O-#*YToPV{zkoJv8A*Ny7!nW%3J;-=1?%d61@XL83!TvAzX3c26B9Z)T zqv+z~E&HD{v|Y0XGghkCflX7zW^B`C75J|hx)?v+#cUfC&_DTA z%mw>VjuiSPgg=A%?$ zyMMFE6gHA-ov**_0$Y3)xV@8N%>QM;z8nqnhc@}xNS2VBGmHqPCq>J{_A~{_l*Cu& z zISpWDX+u6>b!1f8Ow?vCcTU$<6zkGy7Dy}dWah9XS2eC~PHlsgalfsaG^kI`XnA7h z3CvQp!duMjz_AmEiJG!KZ_aAK!Di+cQ+Lu|%>Md#pU5R+R*hOZNJ3q+%rS2Z=1qd9$BfvHWdoP zPKv^p(C7Kll0FoUw<8h3D`OV{GQDF4C4yymuBR>!I8OQp(Sott>{S)~$L7Aw!8swe z>>I75a>?b8G!r3tbs;V}u!O;?tSl0C?z@*^1~<;|eIl*&Jj#Lq(^V3U3W~i%x(Tkx!Xr>GjC8f@=e2*yaGeI*FsKbnNe@I|$TZTZ=K-WcLZ|0s zm*xys7ky92wT6(hKwAsHCQ=!ac%l3O#i!IwI zhJs=9g0mLDp@O3YCSGM3IaHP(+0jW}HaCnHIeaZke$k^T-hWHV^rYI16VN6on1!xN zrv@W7TOb{nwJd5vbk2S=bq%L=4Pj(Xr8w`Iw`OI!^-t_x4_vT%EYS_t7`FWq?PHTI1)2afm==BSuHQ)OR*dJxkfSs_hc zF!S~AZ!v@=ehKC&==`Ps?lW(W5e)x{>yPw3f_A~+7f#aHV>A7Qti8dG;8Uz~;8oLT zkQ3kUR);{Q5zh7Uwu>4w#kS?m09e*<6Lgme`>p2!PE|jB&YHnaCS(J&agb>(< zkhYrW+i`32VtmI!S)`~yRwJjh753$Q!R^P-%3vvtWQv7xsjMcM&>URc^#RG0UU&H# zyxb0K1m?m9HuoYn62a@xVB&XCnMtdmzcTrv!jkvh#0)M6(hF;UC!$Vj^E;FbF2%@r z(GQ?_B)m$A{btn|=GzdqmNWpsoTVpBEX>f~h6NAyM66N&a-s;F|J*u3{ZYPrkg}0} zKb3(fcX&NXl#=)*w6k*53Jp{Ab17$A7#-*@8xK4Q=`-g*!x*UxKUt$Ou z1z+Zj6XpUkIT8|~w#m__u~5NP9ou1=$z`Bm`2eunW@RWGHqHR#nkT;TNXaC4g?vBA z6vL?>kgU*}s|5pcV#ZM?g`#0492jFgaRQAKarEhNM4|viF(mp>3dts=`QX@cC=IGZ zdIEXdiytGGzi!4_B5&joG%~ZnIu~f`JdzAiOnDkk(H;LcL?vA-*qMxHFhyO+05VoV z>Xh&gn+V)!0JkdO$s$Uva4fhAjjIY*M>hAJ5wX-pqRAuI^lxyaTMBhl@PXB6I214? z`Z|s(bFzg>T^nv!LKwJ)%}s_PNlv0YINn8$$*l?HVgho{0hkKwBbVZS5sY~z630psAQm z8OS$+jCO|Dr6`IEdfLSj8TVIeicP9BgSqe5n2ef|R>k48J1N{qIPQLQ;lTq__q;Cx zOVqdYv?m{n%8W(F-7>y9Pk)0?U8?jZD0f7RO+2F{WOv8->Zb85#jyOlz{WpdQln-O zpb$q#*x@*Hy)?88AD!WgkE6riTgvs8<&i&`*r<_)->cu~#{JT)$py?s0yPn_K-hj- zV^&J2?<5-=uB{r)zZX*Q9WmENP0V(G0;&Q*fZ`J*Y33vNGqyT-e>PP7wYFiQIvWuo zGbGO~<&85?my=wNOFqVd7BhQ(q9|VcBUp7~7F3pf%rr&WmXebQa4~v~rZ?6Ym6#Pg zwX``R5Jix_&DH0y0ISp>ms)u9d!9QynMMIiXH6x{Pn9)IG&j55lZ)^J)7%S?ShG`5 z$(cX%mJ9Pos`O(kBkzIsJaE1QD7c{oJ~S#k2<1Z8oAQCck#9y|be}^FmmE!i><+qw z3%P_$lIL2O5WtN(6zydWWLpP714%ARQuSJ)w zhqI%lp_g~3hC`|jB(CEkj}uTi9;~d@G^i3W<6rbg#&<@h@aHGI;>=Xt>EhCq7(gt) zYr~h!i)LF*aOHyg*eEew>WeI67csUBi0flLFK3AT1N>+S++e;LW^ozT{{qD`h{L`D z=0r!OUiivy@1|)#>4m|UFiqj*QLRKedUJAZpEkv)yHZ(BH4A>ty@*~-_?O|FPU5J; zQ$?uYK4*Mpm^@~tt1fB=+R$10{itec^!_T;%bYy6Mj9SLQHL3lE zle7Hxha)bligEA2%t+b7r~=JsoXHp(_jui=z%iEuy=qq-FN8v^cc6eR5J=sJjUBW8 zMd4dN+p73b3Xc&<+~V?F%lu!=$XY0rTio!+eI_3gR8|qq+&?v_~ywhVGo^5nB6$^IjKfPQ6mt){jB+l7K zXoWO=4k6>`{An#+Z$6SmFtq%wnqfdWVy}8tr}l{bNiBm$Ii=OB0yU8HYn*_9GDY(O z!i*`BmVSX-W7py~u5z%JuM5-3bBq#?APT2#^LJ$y-{y}q(=$;f$#cT5-FxAUkw!6q zH5ItNOs*Vxl;#2P?MX}VpLkSgbwn|*l~<2#vDxAgV+Js%Csa-aCCM8tlu?!~rLlg`#Bt$bh z*rh+{3eNGx(vUW*UaP5orv9Q|UzbJqoP_*gOs=4(;w~tbjHIe2?QV`qG1pj?GyXJ6diLL1hbHj@#J4 z!^<3Z$)Mh2(XZLk-yoFB%WTBZv7E+|1nNftu8rYZhN?t5#OgH!S0AGEw-W%0z>i|q zV4dR&_1O7+;u`?wymqn+<-5;%By zMy&0G#?4n2wrsdR2Vmz*OKPIPN(=t%MCj`xspXN(cX@2+ZNvQAg}niXDSdPYvW+h{ zt=`|jkc68Mq&Ko3m7N*NeCGNR-UsFqn?4KGIQ6{@XXmMBKN;E<6Cr_bmfH-fN0pDq ze6j0tn2AE;j%C|!j4sKEG>dUqQH!Ks%NROwpe$`A&bV7%Ro51jxt$)86u$S$7)h6^ z*Q~i0jAgR8WPV%!xDwxjJET%>Kc@*~3V3f1@uWjY&AhXcYREUrc;AF{y*=>z6|G@1 z^s|o{T(MvcLW0nbe4Vs-a~2aTW*+NAd{i_7B>7x(t$Aa5hvo9(2o*_WY=NS%M2FQ& zzL|~Mia3hTk&WmgBA=B@EiAQrsZ=K(^31??z*zBjim~~OmyCrH8VmrEVOxK4sr3I{ zn0V)W;?>-!^x+MqrwGA|kCJemo3Cq!FN2!og+_Fb;eA7iR4%>4Xv}Vy2;IBd?do8f za7^d z$R0;Co(gAuw?sXsHZ&6|Jcg_9MhXII$6^?RtO3=JU0CM%u74I@0#a5Ve754^c^~F^ zxra!%wDpC8-binlAH7JKJ%7IoTyw~U#IuR+!DznIzEoyh<#bC0%J z7WT%Ux8`xM*Re5ZKso?An9Jc8og556D8QA81SLW)L3O6TZ?pg-V+xYs2=F0?hA1aO zWVHE0gA?g7d7O`~n5QxY>=$Ysf3wWwiW?1^RQ+b1E0l`_b30wLEtINc2&AnwvoBR@ zSI`I<{btV!MP`bpUv6BMQAZudW3qQk0VJRlXuF)=a+|>F)^Tec8ksjb{qL@Rp5F0p z4}@aSPERiJX}=TzkvDBCZRlo0EGYfc8XYZAmCZ1mqB`~Ycjk)k0MG&74>juAz)+f> z4?<_F&Gw7+E=w&g$olR)*SSf%?~y+5ys+V0h0X+O>TZVvRcWwNC+iV%Zp{o#x`xElJ5LTE6_wvkzS; zCPyb-SeQaHk*nxW;ph)2b&6Q-7>c2=SsLtxfNs64Wkt+bkl5*s*#%Ss}4Z3c1nvM_dI$GFa)YjgRWuKxM5m`@SfQSU7MPc&J^Gg+zu zHh%UsO^T67slDSArbHmqiR4t7d&g=80P>vidmeEN$fr*nt!J_v0vm=cv)ydms!ITD z?)F8+9+&oIj~bVbub~q49XshxJo3@(<8v%Z#VRLtmlG1a8Vj?`Fi|K~6*hgo~jHqQD}XUv_n*Gj%> zedo);?x)XxpATPNzaTkuKI6`PLYA~ZMsxU#$}ksnJdt;{&N_&hA0I^2ONai-fnjL* z+#;fCU6t&f5dF5^0!z>mMGHEF)~p%ME%+77lue#YoZvlAQz80?On#J;Uwq;_+YdO+ zIa@c0yfR!2U_wYcV>%v8q=_CE#89=38LY(Fv@6qSsPNOVDaPc9;kK7dkuUU3tSwVX z$W*4HpjlF!k>?)CD4WtHb5p$c5;fjM<*3vZh~4#cPy322gB>8ir^&^{;PRUfL=hLI zV8kFN&P=FG1A|B9MlthNDWg6A15r;!HI(`i%iZp$K$K~ICfZNvNgvub3B-_X)zpeP384Ihlr|x@9v7V_SuVJ>#x{(j%HE<1 zmA)BQF-YeruFg&) z%WJ?~c}=J)DAad^sdg3L{M{t`D(*q^L?90x?;|}pX-HKbklX`g4w2~M?3#P#@%_R{ z+(B&6Iiw7*N+(PPDi`%pfS};qnwdg+z5d-kUNClM#vfmHn{74Pj(?x39!Rh2?=mL9 zJ7y>+wW9Gp&}}||;dO+AEY`ry{>Ke0OOj(mzt=ZDc$VE3!Du=2iw2RsoWNvaRBR@K zr(Y%C^>n~q?$y9Do8*L)S5&Y zbN%WYDqE_hTr@5JM9+Ifqpaf2erToWq}_;4%q&jM1yp9FD{ed!*UD6A(MVOHTshD$ zwnKDde~5RSykMGgL7dPSe!aVzfc-7eT~vW1HfTJ7h}%)m)v=VP$OFDrfpkR<8+Vgc z&SLFbgT!>l7x4&1$$l6U{K{s@AIngSDQc5BB@An6zX_$xJLSB{Ug`Lu)3BG!{JO-r zyfmMzCg!J_?JEg-*J!Q=>?ID$q3U&D)2(ddzjFbkunk1ti*RQP^|?zz&&I*u_Rd9h zSFfC$Z%1Y#y&KdQK9fD)ulw(0JO#myB0XEE$FZON{LX@ZXRazLw}Y|OewADJ@9gMl z2Vx0;*adsdeemvGdCuJk7<;wB-|w~j(XNtGdiPmIEehY|USOp`EZU^Iv^elSWX=3x zK0V14F=sm^KbKrV5Ad1|`}-jeKkrcC?hj^CzFgV6fFBwKNw#{@mO(nEC-2+E%MOja zeps2Fnr=P3`OM>8pCWZ;Tk!B75@~C}^NH;z@qh8$yROM2l|C85Qre9t1@zelm=4|`~zG%W5+*IZroVqOG$Z*4^m0%;H3Hd^q!+uxgA zJF-fhd^7WJh|Iqcar8g1-eL-EI>?v(=U3K(b=qUkFDicborTSob}Luule6m(X)-|7 zmz3IcbCp^1)+f6zP48C{G+5TOO?5!`bYb<%iZ-y!bfuzyHBij6!!#sb^RtI@5iK=x z6xcQtnU_=RZSEUth{h`zQwg7Mg5;LE=VtHbn2*W#M`pTmn{d-sm|-KpJrT>O7S-qo zy95;j-EPzaid@x-3T7IdriYZ;!ZP`?3Xux7xkl{*3IOeewY4MRP?HH_m@+gL(T69s z6z-~wY6#E5E7mW?MBKIwbvuU;ISgfrWhr035Yk;D43mh%Rh`M^NUdk|RPyy?M z8)^;#q{l`_&UuLB3Ej(Rj6yg|I0P4&L(OIz?f4r#a}XwbYa~&nIs^)!96)_D5cft3 zr^XqT93D48!q& zO1k&QJJj4Tfj%NGSKWu3mvN48ekNY2l7X}F9cJo~HEnGz8mND%%3lS_ zzjO1Ax($d@HA5TXqJ_!A$-^5Uo${`+8bg@pz6v_?lF@dlQ%|8W|pU~%4+|c^K zqyGhEh8z!csUd+xYr0l1vH(0zO5!&>XjPhS+|KU#QZl*==YXPsAMmu|-ogs!pXG(^QVrwAoi}?N9ZLB7fMXfsTFJ z96a%=GOskM17~RlX|nRG=HdItnQWo@$+APMj75&;ma4|xr8y*8)=mUXb7Wtt9Nz|1 ziG!U;9P^8A*-QErz9ePmTE%7wJK3x2{1V0Rp2Lz{h$W&|EZm)M9*mo6WiL~yR#mJ1 znwS%;9#3?g|Gq$brJu*8BfU(3|e*A&j^5f)@cqKv$2t?jL}2NaJn>Vj z!gv?Pw1k7?^ztt0Rx8G2m43zvO7Ir7(E|61X~ z=T2EI7A!M>44Z^4Q@?i0!A_t!hZ{m6;i(vy2yi1OEr}hIc>;1e%c7VB}67Rtlwrg z*ap)(vp~(p`a7DMDrBe$hHsk{%_LLn#~;mvz$h6zTbHiaatO_+wJ_<1`EN2IO^?gO z@7>tEybIe#|DHPTw3#bBUx36$tC7gDzl;58N87^&HXfa0=m20j87UD$1P6zsPDS-F z4cOfqQMO}v-x|x2TufqxxAr3j79d7a;d0OEJoWW>$X^Yb-$*mdnA$3XeIsV0VZ%&= zxm>Vy8|7aNs0a1mZRQu>`2JK_n-DvOuKisyQpVbQIB~jXHpi*IZjTO);+_!Aw1>b7 z={T=jPoX^Q=OCM+R`hBO6_g8J)I-!jZe_j3(Nd<@E46x^BU8?kbYX`MieI6b018yo31t@Ys- zbO+!1w`@(gIWs6C{6r`t`GSYXCi##Vbi#sZppyG5^sGI7p12h^vL0K1kCopmsAk+_ zq#yV-rZ~UuCxJvz)fW^m9l0mYoJhWm5Vr6uMX-(24Rh?%X`PJ*c%sPxp&E@}%y5U_ z?eNgP7ASYVRP$AUc=GB#%{d@QVD~>~frTGZd%3)o}RCRuY-bphE)Z%XJ1MMBwxVR+!5x zOMoGWtxy=g&VuyyUG{H8=?CV&dqq{S*QBjcY*+wka|%kxiL*D+PI@%eKXb$g8&qk1tNW*b%U)fX_xb zvD5mHYdulJ>cLj`o)eqQ!RQGovxC?;B?YYte8t%)@0ob zLje4@PwuWhKkCmQr7Sn7pUJ9+vm7Vcq1`X?$WSt`=Iu0diGlc$OV zmh(ok4%(2Rvp34Y8twevBmT|$RMb`1OF%{AaK9AqUWKu82Dg=G$ILI(<+%IZNc!II zjHdn2qLBV1Pm7RYF~0ed1`S3}*jHFLR_OF_ExBW#Y(stLI^o}Ax)N;ZM>YAUQ5;_f zi#SS+`V;vwvR85L1MBfwRHAy9nd%>830b_J+U7P@*fs^{qrgRLElsq>aJ64>Yr~zE z&+Ac)jkpi3kpwSdH^GW9I-N^BxCj_7I>FQ#S!Fj!YbE~DV8i>So|Prb{NuRtg5K-L zSnaM3P368X<2=tjeUo`nYdRWW<kn74Y?MWPbV4CXOC88`f|%`lo@My>T)=lcQ7& z240OXzeZCqGIlAR2`a`TNhaP*j@JZC7)(y|1WbO>pUN1UoRpm0lAPM>oj#PDz381? ze41)#n)^LF_f>N4^lAPs;Gaf0k0CYxx^G@YYC*McL8q^OD+W%=VI71opwTX`GfiyC zvGOZt72~p((y?blqgP_Kn)^p_Nm*LM^H7ar;bh>qkI&y`rF<5pwl4a%a^7sA{M}*= z_)Z$MP55_bBXD=`dH1w$yW)CSN!kRU=9tZ-6F1Xw`eL)-K{iA!_X-W0wOKaEiiOp@=j{ch4E{zW2VL{XY9W zXRY%OuviP$wXXU8zH@y#GU6Y$lHcrRckEW*?wWh=*1y>Ud;j+I-ka{&`_{4D`LKW3 zv481(F#d43_GZMLK1w%VklYR|_uw2gxUfdUgVJ^)(|P7kc&6<0apkDV93o}wqv6;o z>@0HS(|IMobd@i3o!xm|4Edb^xykprEr$%(_}qaZcfFk#!6NrRJMVt`Ja~$TB4aU9 z0!1mGp^!*wt8{e((aBK*Kq|j_!*Dq0Z2 zH$_s(6zys`eE6EoWj@!|QZk;OB@n!f?nGxXS*%*D*`C(8F<5Dg$xEK@@^Puzerv8h z-SyMYP9PE?#nrjpnsTq&n~n^3$E|VsIBCXvQ_1m}-0T4g?oFG6h3;U&Pde^Lt9;+m zoUA8UV@}q9#pdo5-k)zTk6AuP8-57E-<+xOzC&aj372j;`@fSPwl{D=CBp&39I2#J(0CGTRW zD4cr1*J4IVJjTLIYT}v#>cq2JIuic~wPB`S$7)(~8Z1#sY-}vm(4Z7N0BT}Rfm&sp zf(FyqsS%s$(PKO>br?1Z);M$~oESebHUtn?2$$Z1sRAqo-gZCX#?0@WOxjpc8bu{P zJSthaFs?~?E;@xNAaAbKIPi@kb|~YUMkiUxemqe%OoYW()8GNPoE$D1Ec<@W_X^Zw zVwOEev+sZ(*ur><*SWp`heSj0nCLAxq$QKjIp*t$xZlFaBEIHT0!C$)j#zd-zobXU zh4HO|??6y)QB;Ve9^gPQnx$ncSa~bJ(*JQ-KgD--me%C0V04EUls@3+rTnO8A5NP$ zF$sVr<7Lu;HEF1#VVxZ!_ZleH8$Wf+0ova$=uPGf8ZFtjjQMqS5KMF@F z(93t3Xc<}j;_@ghhS9blR9H}i=1fdYg!V#eb?E+jqgv$i?b4vg!|nBWC*=N~P!t9O z%Ns%eC+yE)(VP2mUlgYmID~moWE>UXO?(al%TU*m6e7y4Zblr%1sAqoWPrXUY*@8dJWW24yA&n zI5s8hUw*CX)}6nKUw8#(CBO`Ce}<-z#z+nYN$6x)SBWQ6aHmpYIilp(NPiK@^taes zrsb{~9n6>SzCHfvknx4W8g7Wf;}fOd*&3BkuXJyLWGt`+VmMV1|0-aCGdPh|7qc_| z3nn+4Wkw&=P$3>xHdKo*fTyE>Eg+GsVy?>NB};=%EUzPYVraS^LiJ;CFuAYHt?3M) zqtruSj2NFnPh7+mj-p`7uAVx2MyHb{8)re`NEgHMg`)BcnEr%4c@q97Yj-G*c2IoA z#w&(*9RgXYmLJlUWVZzDPll{|;SC(1t|!?uH_$0h%Q6tlsYY&okitF9RfOX!?ulShh8~*k)5c3c7T7f@lJ(On zVR{j#5+4y6j-u!jnqZ1(Q(X3SQ~5KA#t&HM67h3#RY`mBCZ-E+&ceGg#5i^(zE8>4E|QK_Oi5=rU$!YKKf}h9=J$&|O5^%WsV^?; zEpI1r1~@)uqDXW>zr;32Ys-SNeT5vRvJiFD__AXEqbvU;!|`+?Xy4oxc&qnokr&iQ zUiB$5{k!kYsqqK`qkZP@@r?(^hFK|jisa(B8ADqmy^vHB?J^vR0qAE=71}%L^%&hU z^HMqIL%e22g8452VP}yXc*o^mF9;GlW%oJM@+~0-up%ECO>jj4dZuykKTXlE8oq{W zZzLW!g;mhIW0Q8@WFM(D+g6hu1&d{*9m-kjvAD-dImf>}asOJtt7;!&O!mI*$f}Rc za#A;uI#fmAiXDxh1g15Nm1_fg2u>6~v?FLC>vMS*Gce0ZOJV6nvVAjQXh9-X>Ic?O z0wKBw+|1*2@3{u0(1fY=!nI5YD3<7k=wtl|f_Ru(6RN^x4DeJKOh;MPFzn4p-yeZ2 zW<*Ip*mfrD}8DBaK|@P-z$2yN~j$m3$iB^Qn7>7{td&*bo) zmrBu7+Q`|bw>2PtAo7!+scf}l7pu+_;xp!iY)XF16p*V%nKsqPb_5q)D6y!g4r4MC zn`6W7Cvdcyrvmg65}q&CD(dW8YY9w`jGfgO3o|Qy)-6ZLVNu*B+JRwEFH#;h1Y6Hw zg>-=@1xCMAzPz3+{6$*-J9FHtS=`i)250`+mWx79U3EKi)wjdcNQZgtT}v4O#&29# z{aoxRHij1OkmqUI-(O}$;T1NLkWEo~46J8Y>C%-f+L+~aB znW$lD;tit#?eeLFSv$MnyQrmUg(lsS5lCC0Bp}1d6jU=~qOd9F_nI+?e_2j9(MAma zE2A`P@Mm4z_Y(mGv|Ol2?+16k(PmlZx;r`#ma9`Qdi}x@C8uv~olDllC@MtRZh$w~ zul(lH{Ve`sNFz%Y(t;{P>$?hU$RlS!QD+0j(6NPB2nkNT-l;1`0IiqhJ@7uq!lQ-V zxk}uPGow|%Yz*aT$7A$FUkWu?!osxjbPxrCe@|A^Q!S8xBef|`a$=<~srSJ%-&jwP zTrWeOe?jF-GDOhOHJBjPExAsq5~dBHk=93KCqsONqkuX<3D&SxIg&Kfqz1cK;C9L3 z@QUH+8mqivJEIJMD2Q7L(<-IVvO?f(nKe1@S1nZLBG?xrOiSM54Tz#*b*V)a8uS!U zyy-!L$WhYg00xZk=6xyW4|MZ#2Jm{a=W=Nm(TCx4y44U|&q~!b1IUMr@U+Eb08}3m z)}+Zy(6If!gH&LKR?rZPAViWzay_cL1;}sx@N8-A;*1pFYbfLTjs3d54~-+hs+z*v zvGmw-!{-nvz@1JZN^5-R*A^4gF49EoX;|5n0x-M886Bfbpu}U!}{r0(VU4NrPzFtLD_FCU{8e zHKDev9-S{UDC|mbrS! zgPtyq76p#7Lc6L!B5p-0iv2BF-bvNCQx7$2LB13j!dr_;g8>#M3v3NmJFsCN zmDv&PMS$QGDitFamj@SRVK8NBp45CE37Z6g%zH`?%;#DrQf_px8Ur|yk|c?ycuhvU zL_LTIMyPRJ5`t2=3LDfcChs50C@+a>%@oR|F5usi znduFzOObI%tAxo;*-=6O#@z>!3W7LPJ(6g?t6UFU%%;i=wPp+&h{p$&jOcZuM&rrM zgB&%LkUG;UGilUy1QZo3AvJ3bcMCdNI-NO5-1RbHo7ylN<_h?p?L1Mj5DY7;T9`FS z6}2#`CP~8(Wz;5XTx|qQcRG{-BV3h!T{wmUZ8~xWb!bV4s}PsV27&9&gv+{|Znf{N za9>MC#Uh}R3ul^%z)^7YC3C=u*7y9)MP=yeI4}JV-E{@x-yI!%BiGz^9Z@5hmMx8p zo|`k*b01O*j!$Q3a5BdE!h6;UNQ=RjMz--QFwFsCC>o->BABu&nA#M@(HbO*#YJ_f z&8Fg*YXX^v>zN^AmC_~KYy*Lt1p8CWqiJt21uRxiRmSH$m``o|MPE0_?62Len+Wr* zOMada-a2G-n-TYy(8QpgszVuVL;2cEa#Hn~U6UQR}TuvFhY+qE_zvN{tPFjoI1%;*{=$e$LKmKHyBv=L4=L_gadQ?Apf} zUPL7+=dM+B1JGHd=z;7(q53B|mDC;XU5zyi8cn@(Eg+9}AP-yFraTi51Mj)I01q=a zkG-3%b}qgsMBF+f#U{ICPgJ{+sg6f?v_M1elk&#CrZKhZ^F$68d`*f(y;&ZiG@gaI zbAw;b9^8!`Q;ZsG=Z5_>!VK0qetEx-{LY)s=>uNXA2k|9zSrDfF&7xBnf_&ylFrxrSB#i0UYlUwd+K1s^h62i{w{Sn|-2tkFsm@pb}&rL#09 zR(~@yuCpK}w!7@SPxdqXxGFFjeWa0SIqJe+*EIxhq%53oHT+(I7KR-|9OqS^KDzwU zJU4w@03M{LoHt+5PLTT3bt0K|*sBR?%jCBSXSbb<7& zi2c&CTcx|*?^w&!8@7g#w;V8D*}W5&83;gsMFj`i%GDe*Vcq1xh0}aZpNFh|SkS<% zsak_-qG7V`QGr06@QfN0sUNx2A(4=mWC!Dm@!p(?Sp{=b+OBrgQA@{ZQ5i0B9;Iy7 zE>5B;MR}2@o{3aNp^43h-Vi3Z3OYLBmjZ>nELlkbXJ2SjOl^0*c~K=e8pU$*z}YAB z`5uc)lyIqu#jKD_k3b7DpuY$~vR*zao*A&rR3fEK>8`kZ^XK}~3Js;3n;ApAH zun0k*~OXXhp2`I#!RGr+13epO~rvM?6?tRxMmfVO5<- zJoB51ptc9Q96CMW72B*o6=R_wuvOK5K&>MsulQ6Sb1nL#c)OLo`@9+gkW%3&q1M(i zl{&AF9-hYA)$T$IAvOmP^PCploEk&DeXe&6;}vV1QMKGRA3781a%yVk(=xQT;}AR^ znv%s7wS@ZwUDK@TD2pD${T`saqct_933bs;nKG3IhGP6zdqQ@hcSAe;tTsJGH_YjZ z6rO%U$wF~y!_=}N5xj{sV1=(YInQpja#KnP79)m2{mum$%*yaU2W~(=Z!kFpN3(My^U*JF!H3|DfYHB#<$TmNYmjJc<_d zWqN3s1siCM&j<9#qEJj_^YPc?tO#@<(YAjpC6M>J7n#l0y7+egiRu%Za2dCvq@QUV{D#M2Q7X;S*uC zN^aPGk`^DYW&Z0-IFy(8v|j<07Jqj6<36yqf#&N6;#r`i&=W5aPs{%2OgP&5ew?TH z;S#1Fgz(6%t^4HpD4_;N?0whqQU6yF5ubbI%S9-a4Z-S^+ogwP#CG6B8TtuCrY|wl zePEiSbVrj0kOFc3kV4wIKsY;O>|lyF@T*cZx*8@@3~C9hk}Ou+P%3+XaqZu{ggX2M zKUN^-HDgQ+HnuFSr1S&7YMcanscF0l!F2(Q45Gk?L?N?MIwt__44s&PcS}qPVl0zr z%JWndGcgP9f=yAgCV`@4o2*p&OzRFE`YgM}9r|oNL}m+HQE6F~Tn&nV?YuBvE3Euz zL0znZcsY-fLUluuouWjCGRxu|Z|2>cf}}7#ud~2n=74es%@?0)Iv`rv<>dp5Wfi4$ zW#yF@8^bKs4HvB}Rjr7md)0j;*1L5h9nwrvlzQf7*9$ zGVpIdpE5-=lV7kXe$Ds+DlJ|-l0vS~AfqVA)^w!uav3QK(tG*Smk)Y{U6obHZ6(Bq zrz$esb=GY)Muz=*Js~=yZ%vm<_j)tKwL>KYN@YJui-KAy*-&Y*r<53&$YVWr^A9SU zqIe8SWebr$QQ1VJ1OKA3|L_u*1GL{}|L_u!>b>P)jt78&6}2zDUPSf7jr4~CF7>@Z z4j%=B&4d;3=~m~9r3I>L2xLLl?;aHiN@c4sTnOdBVTSm^vqO0aQ)GKUEef3U-=q*< znmd*m8M?877ei0MRdoR42t}FNP!B=t(Ib_O{==mwIwUmUL@X(K{ zINW8T+QG?Sw3!~ygkRL>@NAn&$S+Q%T|80QT94A=MPtnqmA$n1 z4{6aks<+Uf!yAIG)bBn&QrQMoULW<0dk>sTn+(dG39Wz5 zlFUQdvc8Xvt)8WUL5z3?*+G2Z@A&CnjY2=Kpl8A_MyqEhpl8Bu`>G!{b}XJLtiZ`v zw;(Fj_z{k*APW6WWgCZNHAAWFBW3mqsI=%tWsi>%ETZ3)XOz?@M#dF}f7OZ)cFTbA zwl{gp`q!CoE~AmMwV%gsG;9a8HkPdGsTGN|yFZqE!}k)z8LVmj$!Nku#)j1~ z2x3y|Od|tQBvo!4l9qxtxT+5a%`Q@irv5$?o@JZLM-=8K3hXh6s?U;1-FgiT!+kLt za*!0E(&EiTh|=HE;$#f;kIqo3N3DJ{mBL$Gh*ZC$;CVBh)AYeuLb0uT)ZJm@IYk=F zf;qp4jz^`7V}z_ho6_{n9LQ|6)N%B;;qA@0E{vv{V8uNXFx%XKQd3>>=$@I-?E-5H zS%Y%17FQLMKvPyzQ}gJ)?Hc{k@ zp^GtB+3w22nElqDQfFvgNQSE3xMU?KZ$L|Oxko`P4V4(tpgzop!%)4u)#Wn&fzZs| z$VVz0vt>v|=_Kw_3snpfCElYrc9I0X+u)R;n=l!B5jlOg$v@sQ<~nwob$hq9P|7zE ztmO1S(zXrw`F=3IBY%MV^+y?>8(<#$d70k*uJ-s%u}tN8l_z z5F8Lt<&vQCnuGU~r_((9W$Vk>1D9x}@2BkKZKwB#zL;$*NXl0|qz^|S%5AHJ<5vSO zIrgHW+mNI8M8B9LpCq|9{Cqinef$f2IxEY)E%TzIJ^tYg(NyX+pvQRz>p zFp$$ZzAk@uv546Txx!vpu88vC*vEe(Gvo7c0j0A2Kl0rL@NC~h^f>}1#9WD$Q$AxcI4e*yDS40lf4pER9!aD30B zr3B&6`b+i)S!v^FT?IMt2UpvhQey`@A>+8E1bfW~$8w=|Z3P3Ug8a6z_OwDmQbNK& zArbQ-Q3l!}0aqb`4D`!{p>h0J-?xBL$hN^H#7QL|)38EVeg&})c_zDsm4Ij_c>+oS zI7)*d74u=%VqtYDI>5`&Qc!RsD7>wNAay>xizvb%H@rtXV!$l|$Yp&`i1laVe1d6^ zVV=WuKcoM@Z=A^g1kEy#jS9ZR|HsDpPnhPv+-K)uiYfaapqYHrlkKg`TKcd1?0j@_ z)w=(-z5ls!;=!){K`fta@0T13IV~>%V_6d66pDY_-v7CALP4`X8|Ncv=4LR67)z+3 z6w`8C5S1yCt(3{W-S>^yOc`uoj@gj^U>TR==OQBX;4 z2wB{GZYX0^iAg9+*1Sm=>%ewi1m~E=`$+zd5}HV%<@xtf!iYQhF|=wH>#>5hZ)4)% zxddY3;l!AH6Er9^3dOY8EjN<19ZEkW8+kLC#T&*E7p1;`qf?Y-UYBZ?VBND&l<|rB zZ^Y6iEcqwU_kk(aApo&8J13Z4moYD#*~%h6<_$Abe!L(QG)pogVJb?u(tX-EnTx%W ztjgpms{pEH&>WIVqM99oS6+@enif-u{L3Sz3h~$@SE2(4Z%?!rRigrCu*?Ixac1E$ zmre+hkmpQ4=4b+Bj_F#CG>1ww{;o%zXh`3Wx-bbN?D|lZZMb>~g(@aGu3h%HB*-x3 za&pm<6i&W`q-Agqhn9Wk{z}!^b~3{KjqG%kW;&uCm=bq+JPweiI-9^o|L8O+jTY%N zCI2F`VM+$hHpx)Y!~0&2Pqx&^HjM!mzuo1Yp{_{R(+e<+d8Zap1NVBf{>!o}g^&zLD~-%6&8A z(}DX|jtILH4(rXx<<1OUOaLBGzNZzhs`6Ho2C?d@LU&n(ZwHlzdaLGQC@ zq2umD@3!Mkv}~{AF^(6XPlwepKc5fDN54K@(1YAvF23VqK@(lDi_lKmp9>kymRw@yczDyw9B5}Tb+Krgc+tL8VAkjGlctlToJ zQFoG974hLzzl2YmfFVj}0p8SVsCN$fgcKD!v`L`|&{RWYhAJAWqfd?3 zxZS~^=@`~?yL1kPVP56gMJ@}!Amu_<;%*wxSVi9={J|*gVsh*Pswm|4k53jx!J%qn zrRRczX5-!`r`ew+dtPq|kRs&T0uS1L5X^HSGIiOabD604krWp3(#gG%Xh!jAc#jSum0~kN zoyfa6Es<8-U6tlS%@I7vg07?*Eb)?7($i^L?^-IyAkdP8n2#vR)N1EH}8r{;FfNIAT(sBX*J+j z7V~;k=`|m!PedMm`a}=?UTq+og&%%_Q6Rl^EdraDCR!GGpqf_r%v_y|?|UL-rVuL- z5F&lilq+T-=-NlM6%7g`9938QEDtt=ft)c@z@Of)Twn~+y(&t`1uLO0G5m(*g{ zQ9?Qs{B&Z(rkYY>8xu^uv>|qvI$-96y`WyEQfO{>T6t{c*|tWx1sZBE*$+y;V_k+wgNC1v zoh0gEj4?#FXxiYHJJqX9UKE{mFd0l{ta)vTd~2S#R&>h5{IM;$)i9F$fWM*8w4vha zJ~NK~w&?dJb1asx7wOrNtu)yh9um0!rP4(W!%}6`j8o9(;)^DUiG7>^p=TRo7tKqr z4_vc7mw!iHcKrTv=mi0fH`TY%@ z#%Mk_(?Hy|B|46$!k5qZ%zt<=Rg;d$)9R%BO8L2)9KblSe4$!$zYqP{-+y;pwn5sD zJ}cj?5Wct?deL#5oPD>((s@1N({Yxme7~*r;$}X(r+BpS852#SCRg+?Am9wwv_fxGZMcN0w`3OjWTeKi@Jc!k-KBVWK62HiDDfgy>2cV~g{;zWK`ZsDL5 z?9U}85G+`jF52>x@E-n%XQ%!JSg>j@xlx7Z?5A-i4(*H z5+rpJq}>w$sR?o|2?`4dO4kW0#EEJGiG(^a8ccEQvShq&iTbjn3-gIa#7QOsNv1kU zX6{KAsYzBXNj3{fw%18^#K{f<$xb@SF7C;0smUHK$zBV|-q*>##3_CPDFHeuLGCFb zsVQMCDX(&Ak-ntF5U0ioq$cR3Cb_4kq^72|q-HFnW?iS|5U1q{q!s9-6}hLCq^6a% zq*W}WRb8jm5U1A(r2C&GHn^vQQqx;o(%Tl&JKU3_uhWaZVD#u@47g_urDhDbWQ;6i zj9q6;5NA#aWX|Yh&bepKr)Dm;WPV@BT)xg+A|D0@ug1oO-uIOLiPhw%Em=DoL~;TZVsYHj=B^E!XyUrO)}0x4i-tS zY7kjBP7Y350)coO;bP*jaU$tWE+t7GwO}5tZXUfy9%EV_b88;!VjlZV9w$jYw_yG& z-F#k;eEzh2!Pb1?#r!un`JyBR;(`T|x&_i61pupz>(l~;#e&bjVw6zh&xi}_s9!77Yhw<3XMpLOazNeb&JeAiY(HKcnfo_7K?0eitI>=9l}yB_=^czsBEl? zLC97VWyOmYGV;5{&DtWOP}7P45no>J<|Tsj2ID)7Jqwlc^Xm6f2ve{LI{0g%N4YFQ07{Bk1U3zqe|?Fl}@%+ zHUk9l!dUYLxL4D{%hIa$T1mSXtBy#jPXw#abgM5ss;|~GmMFcJ1YAqDR7-JNOSxpI34H?8tE2a< zV@$7O2G_AJ)v@2!agx?^3)R2UtLKGy*7K*=3xex~m+If#){Bxhhzm7HTGP(zHUQEa z&8ktiIHeL)w}r)LNj|TIAVUlHOVdZmn2qt-5WkAq`C; z1UKk`J&E<2(!s4@aN82N;}%RH1nv@Q>${EZMj_HsX&VN&jmRg!PP7G4wN0=PeLTVm z<7)>}0j9#+-`lr+zinT+C+#RqZCTap*!1k!PVd-diC71B9Ncysk#?R4b)M;ULa)hP zrFY(dJMWe{A8tEg$hzQ!yWsV^5WTvPGrCaQy3oIOVcd0LLI3@QyYck93B0lJ#;6_r4+{W!LZJ&*&9I&&me( zzCkB)F3SWpB#P+w#W5jD>ldYo;(=I1_|R+QGy04t`_%ONHNE<^?+6dm`t`r}8{YLB zkqwv#514)@Rn{M{$QZC{8?gC4V0$-UM>gmnJeX#g>p<4$lriYhHs~b``n@>lOE%;u zJQU#7z5lfp7WRp%efCJzBEX=+{Dfouza|1w@c&E%$YksOYa$@~66NU)iu^CC_DO8( zf)eLXRPBEd=YJ&vWd>iHD)mZLe*S|v*TYACm#g}KN^yq^^DE3AbIay3He>Z4iGU9E zmywqGt)L(%asEFO0TiyDe-P*YP6R;5YeUdCC~qAXi7ohC%mntB8|~j_mDVDlACxYe zE4@&$O}V4ZeRONO{4g`c==S_*?Q8ZEah}Sz_H(?m6Ow&+&+tT?uROj%t@xqy{+$Tm z{dt2b`4vRwvi&RgiK?Yd0xc8J7lBBHn47juw1Mq^-k@l7y?>OfMG60ls+A+vd2#JQ&B6IHvBr0=q`k!%zMCC*Kg9;wmLT>=LV{t)MAvf_vBSDWRB9d{Q{sul)`^{5M;(B`NcnW5sS2c2*4s259y z^|%jD%f`MWqRHd&4QlUrkn-sbD*1`3tvDHGeWYqR2U$-?UVW=LEaTm(aFk2K!dn{? zMPmDlI0r!Apr#b~_RnfQzxkLqpr-XioL8RD>AOOSbE9B3C~=7I#n5@b=OgWQdo;))qUB`G@^D8f<=2^Ud3Xp-KhbkwkO0s(C@68RkON0J)P*Sc z58|BD_A@xA49Z$O_c-#W)JMw_2Z*0)*eX;4)Dd6Sv-r1F2Ryz(nFD%B7LS6Yc3xn( z4)s#pJibBYVL^fcPj65Nx8yjnetC3)$Dt|_d3eQuerDa{FaljFg66AaT!-UuBlLWt zLBIfa+Hr)1!s8p%0B`GYr1UX0*%n|>aPc_G4f+OkIW+j@=IIUUpTzmrJjFfR(;L*a zF5|!6pn}ypp?84ebI=R_euFAeDE!wOR0BFO{85kg;y;MfrWTO0pWxO}%>OIPb0@JyR09|RtGPV1_nZiGBP#RqEW2JAZy?GkT zG@HY!G91p!jh>fl59)Sg%GH&aDwgYhTg0iaJI_4udLqt=5D_b#N6QT#iSw{n*PJuv z2ROKY&+|ut^VhS#1YN=l2Y{Ev(H(qna$+7B}7#myZuFve=3z;w~Ot+ijPa5bc4=w5{-9p zoSFiKKuoR{tVAt2okHk5KQBx%ZteS^t1G>e8f|s6L6dH<;TDsDIJvNy>9!e4lHqaj zAM?Bn8Yus7^W0^3r#L5wdH1h*UR1VQR?)P(`xiYPVEZFTK;jKAl35&p=(=Q-Cq4Z{j(^Y_EZ@gVLJ%1Jgx^y54~ zUAG@*Jq2^CUN^D6EP|`nJ{{q&FZ@EVySaBdE{ddRH7-v0vEiKwLekkF-HTCzNoc2_ zT%1e(V1iBCaePGaA-+_*1{h<1;?;dykZ07hAOKGm;KO$H~67C(>>hw zW~Ze1joMmS^PBbWX<&9w-$FtSp#+`F87JrH@dIe5pc%vY2wz_G?wGyF@9qRoy65in zPp3fQ(*1n?Pp2T-0oo~0*4=Lpf12mfCkLdBl@HP@G!NcN8qIwVcg|y_A`f?$(0Psv zegT8Dopgb9cl#$;0yDoomW~XC&^vtMV!I1 zBPJw|odW1Q|MDN50(FD7BHrYC5D_->(nKZ>&GqUn91_2_y{z}4= zvg00xVTuP0#ijF>5K>jtU5~p0T9_D~JVy-r>>HcRTR>h{KoLOx8QZjIpCz%P-C(XL zD&sc{6Zr_Z1H<*x5&#!}qTA zUZ};+R9GZ`S-|_?(jddaX7!()0s_;zrhZ~u9<_zzc1;w6kp4aUr^5`G-8 ziIioh52s=0(=*N=irQMPru7{!d2V3fWDPmgaxWou zU|^6Ul`A?}L6Hjgnvs7w!!j-c%lY7HNEPyJJziUbs5SA=LnUSC=xV zU?$__hF|45IYo=jIoHCC9n zV`)!&NHldcR=Gku1^x@)@T_0eh@Gz+litn`DP0uMSMHgSMl6hBsMh9xF3?xMU7RmE zZm1`xvvy2x00H!D}#Cn$Bu%8yfluyL46EL$6?S;0TGgx_d%vTsZ{O{a^(nu5X7UH zOC(r22w%%EL-I-DZ`xJEP`E*ZEIc=`77JzZzd8k?n61!G!C4;Z{Wd_kb?PsATzDCN zrda7*KB-~1%$BRupU}3#^ZvIHCV2jZc&0}5{T^O9`{FkxXC<4$4Sa!yvyLGb@uil7 zw*nIyII6g%r*^-w1H>teZ9CLx5p>N4J)PF8-155&C|5oTJ2|ks7h!drxT^|n@db*Q}K zuY9*k`qR2s2^IMDOF7K&(6ekXbETECP+k^JRV z{W&DDxGnsJwFzp>)v96*35@+J{QO?KJ$4G@KmiKKoQhWgDnx;5{DB(tqH!vL+9`p$ zpg?_XFUk2pBcjhDjNWVjYy-j|iZ{*(t$YJ%CejU=C+T&x7(De)KuvP!w-$}JFI zEd@z`M5Yd+O|mfY!OkjK4P(9GGYQv6K+f0U9F~ux<#ql5gxOk>97pGO1xO8*Gy`FCmnEmL zE_Jv1^}gI*EnJNMUfl?@v|emGFUx*|U=aYC2SBff>T#I;W=s7MrJF$zjc@w1d7FTk zT`HsK_W_s+u4p@Kn3ps(`*%6q{(nXU)lC6c3-1g=H{j0{6& z%oH-$=a^|kF@u;HX^xN1(^?{#&NF(-KN`oOd4NmiZv>LFd5bj^PI2oOGfiTj2=VyD z9MM!Rf1vytja|0gg6#7x`CLA~TJa;qa9s(;%>w;$pjig3M!(E*$rAmYdbpm_yHFW| zVjmT=g#@&X*~b2?9kb({S>>@?f#%!#yS#V#=6A!O;q6}2R@Ci5-K979u=Uya?eBbZ zTWB6o?Ph&4!nbF2I#KL--8gNo^Z5)}T3Q8b-ZguF0NP3I=(Nsy-Qhg#@6%!at@`lc zDG%rwuiyK6cePaw`LjPh zZj8q~0J=ZQ0D5r7`U4mha#6MZ)k&SNz>h~7hUNikl>gcv6HuN*`~i{#sTCmrebt({ zp+5TmnFl=XkIh5<>>mHh16C7N&Bgr3h`(9H89i`y;fI%J=1q(tphZ)>CO%&1Jvp{+$O1 zU<3Z_kF5SXmBLkkU@T&KF1D7o4;Lt@Tmn%NK|?fSeUqv4BSp!% z)katmK1bil+y|PMkA z&G{QfDKi0xoMbyr5)Z=b3KPPw^8=6jV`PPC>f4M8C4wYlMt~W|V<(ljF7$p6ZGjTn zN!9cU7Own1_Q!|%%BImu+d-#qKX#+5I;5!HO31`aAoBSHy|#sPlzm4jYHBg5s|brj z`*BR%4Ba0OqRJQy_B}4aD}VOKleEjWZ39Zd>95Ndt(Wabk=0&X@;`4DFFUSAtGzFw zoz$DlPMA#_*B9I?Fy$3uBls+KgxvaJue6P4oImvuI2b&8KPJ%AxbM&Bpb-=)L38AF z=}rGsW3ul`H*v#6qb)?c2yE z&odiHW^$fZ$4=_s`{T__iPE3_5qvXS^SD2b?&MG2%*Ek9d)gmwbxSvH z8lhUc4WklhJ~=EYmpIPS^wx5s(vi0yszi%Ueo2|T;{se!xo=C8pzL~aAwE6*E0C6C z{1-v9f(SY`S2?NDDEp8d%l!c#Le4TYed@SupTMq7q1)Ny%SFPo1OBx?dN$iB&>_8k zj`X&h=CA#695lc;b{yp?yow6}4as=#bjRJTS3rb$hqazMsdC-kEhFY*yD`vCs@N{e z7?15~NT1i1Q)@HK8Jl=!`PonNfS2N)7-uwMLiAgOVd|c2f0J zlZmEfxi8{0F(MIAz@OLdoeRs__l0$}7MyP{5@y?5`~1Pnmt&V5w|{j~-$|ZQ#qS3w z-`x;II7Qp(9dn}Ano>gYSL_z3LW+eGBdXjljT*d5zj$q#pE)Iu7%1kwXx~kDxS0hn zcUAhd@0q`wzMFWXq2Kx1Z$>vgkI~>#*PdpUCGKV(NOYCW^!nrle0d~#G2U0LDaL{| zoZV=6xXIwS;ri+T*xtY8m%l&=Vc3bAn05fO zy|=kvz-6(QG(Vn#rkCQM{ZTtm3^`B}x<9(T5d{Ulqw+N*3NqmjGSv=h4hl3IdLb7A z)DzVut_`vz3TDK8Wx*e)aD{^qf<-fD1g*(B#0ERK;Y+4qp}ApqPX)8o`U6YId_f^m zj|~ALXd5*&0U3IFG1%mT1A88xO>jtJ2;Bx^P(Wa)r~^sXJpSNZhy-G=>Xe%dv;}!g zzV3>JtBq6OW|lGd|B(0AVNvLByYGP13?bbhF?2|mMZ?hD(jbk5v`Tjjog&@cE!`kU zmr6(qDj}iB89-g?a>Z}&_kGXV`~2;Ng4giWeShvR;S4^`Gh{61qlcrD_Rs35izEX{ z;X<_3LKN6Tm8hh1r?E2VU9bYNXV?P~BOkbY2yJ!>(Nqj1#bVjZ2%|L!d(a*F)Qol! zKMbiYY~xKZePqa5AWqUqi06`I6mG;(LIilsuOKH#t}Q&(AOc?S@rec$$rXxn;SfNZ zfnpp(F^M9v`6Htcp}2imn=X;eAILuEMH0Xn5%oduNMP-(N7C|>(YQuwMuw;n*pX?1 z4Ur-~;YA@EM#?OPp=cX~+vPVr7)#%q|=P{uyyL=9r2^-aqPZ zC;$w=-WA5b)!$kbFZDMg2>`iB@L%e0c;17*-fIQG*^Z`BIVdzjPtF-I*uB=iGq1XE zs#IUq2K2+d_RVz7#l4nybuLl|ox_^J&&0dI^NR93)g=QKVWnCY_f3D>RX~X2tPIPz zn799M^Cm_&j<=x@h*;d0;g zHxzR(nEn>r9WIjc0H(hM9A^Mw`kQ9rEsKkJTL9sWphI{4%Xpanc27g;LVrsdzxPdl z`w#B5Cld^wg=OS0N3KvMz6w)e-D<&B_Pw3WtZT8gCjo8)$ z=NC4Ypg+F;lm1qFId7kR-k-94>|Qkg)4ctQ{svWe=z2MCYkgzDF6V6zm5;*f484)> zf0?(x>2G|4-|w|`1A4h2Clnp6Vr-9vWg4482w=NQq`2Qd9@ON;jA8sz!R-O3*rFy)w+pYA!YFpPM^=4}+&AM`i+ zd3eUrHO%si@pO7|bG5#({s4D!^o`gI2BrOl$}D3vBRSS!r5b2^!uNUmw+vW@UzT`m zh*UI;0mDGgmkTRUQO_Rr_i!lC7=r0<{S!qx)rI<2 z7X7-%sPt=ilA~4>Ikw#OWIeJnTt?qq)XCfH%nC~RaJ%&=1q4UjQTK&bDPl&gq} zAB07H9rr1OSH$GXz~*h&5Zo4f8DGbh_og$HdTM2%)o-y9qBQHm`S&!f=I6*Pv)L*v zQY{i*SuFBV8xK1TJER0sOGP+A))zyF2XC#PX%iP?Jq6&Io#GV!?LNx-hZG zWQsbj#Aul2!D_nNnyY+sDFtBPZqj zXjLL+2hBr@)Pm+Cqzga`;L90dO;qg+*UPQa``pBW!Dy=07&m2E@<$W>!^AQo4m(=G zOe`#x&vY+LEKG?e@EV)jUrj6=j5akJHL#;qkSY+?M2EYiE=?@>emoe@09&<2KOC** zya&!jPqevsZ1vA6O&3ix#t}2tCGK8{Yb)KIiR0C@4prmzA51K^0{6EhDuJKAg4A0`f;&xz=c4CtOe~cC zNT>VbmnIhZvYuJz>wwjBkQ!!UxmH`adSPOzwLLJ{DLojCOX`+?r$!vMQ+9eGdj4u+ z2`p7(`@_U?vho0bFb#>f_ot&(60C{pJiUSp-+~JRsqbPD{JDv09iF6Lm{@G?<_Ajt z08;;9V)^B0MLE&V4pQ{T7kwo>YtSt~Rvz|Q1Re>eLbx&yD;znBT*~`>)Z;}((f6M+ zDLCt%30;8Hu%lId(&)oYhZrnG>e$R4h}H{_ibmrk#{51xr|TfC_uoP4%O)z7iC7+6 z25X`O-F=#0k5=-0&%6Mp@vX+BV4v@zr``J=Y*(Oo;z(!?t2@IGHh%@IiRSRs_6^y< zOf0X_a(@d_uT}apJa?OQ2vw$+qi{R8p&qN zg`PK2k=p0^{i{jX3`XKF%_d6R@Jx=BH`VhqC(4r!ipqxqVmX<37|GVpbS9-Cie6I|Vb1vdXr($PN-aIp?^amEe4H&OY(A58e}WOC z$l~5prGe3Nka~#pnzDQJi=7qNbIjX!=Rq+GXMH@J_RYmM&I5BVcWG-|&`a+3bV2ek z4(kv%@kkAs8&g39H&w%+g@8XNW5;Ae#|%d82^ z9S^$}wJLrkfm5+&Uud<7fZOu?XmubZA8oG4+xaX< z?tw@{(WcthqZKmRJ!xcwi@tjy7M+`o-hVe60VvAS1ig{asbuQd(mTAa{x~3^8gc9 zwHMkFLSoom$C~T)JsPiu?{X83BsW-|$;c^%0MwRX%j>i9*h$#6Wsm1^)NRF$w|iT* zav&3IFUe`u1qp1GIDpjd6WrI2(0Br;JQ2z0NAS_G!FRp!WYe`pVDET86}l!szZ94@ zv!+1een6*)33l?;mAzgAGmpwp3cZxUtjH2oQ5wm^6~;cH&k7~PxHOL%M=G0(>jq*h z5T@HG`HxddqJF^@i<3Gxj}o$<_uU46&jb@)-yB2LlQ+gm_uF~mHSAv2j3iks0wYD- zCO2}8tiL2`rP&ghn#l!%zAdj0C=n8}KwdLz=J83~bWa`aK^wWw)>4W&Y2@M-`FR3v zW?#)lv}X_23^UX=8AQrq%PY)0Dm7Qo^4&bj1t0Qlc@?)ztax#59wj&FGZC6IJU5TZ ziNbKwOY^Av+qlh|Et^WiCfM@YZc3@pwEv@dluhRdUu?Y$= zxfGQyeAGgSFrjVM-EN8iE3I%UJHpX!s!9U8emw!^tRSq&!QH_{VNI2Ygc+Gi8RU1& zOJPhncvXTzSDRA@Z_|Yti}VG#6}(Wm?qo7vw!~aIs&Y7>{9My^(rzlD8wUR~D&YdB zA@+Ayr*AagWScV!$f@bFdCKBBVYgOXtHw8XrmQf7|GYSF%spbmNqA!B?$MH2+d|@U zE13<{k&V;FRo7cjmt`f%d8b}kZ|`J(?V|5W*oU8N7S-;YY+0LNG`bW#xB0YPcP+?y zXU>lfIvl*y;$V=Fwnb z3Iw{q5+o(4Cy-t%iT(i|Q1@IcqD%y72bDOJxVTJ4r9#ekvFJ{28-o_R9B6`gl-vA z`5K-Crv;_N31L%VHCl1WHq4|qSAv;H4rHugeHZgBEgytoC|GAqdW|%fbB{*uK?BS@ zIxoe)40#@w1zTR3AvgBU`|io!QviCNpk?LrbFt{BDTxKQH^Gi zCu8RAFMWR|dPYb%w+no)Bs6V4rV^TkoG=%*NMoN`mQkg0iVY{gJKQ|m-*D?@AZHeQK> zb`#;yAWR5BUd=3uW%k#Fp9h%8Zb)gv0!%K< zMi*kyaVO`c+307n$R;M)s98X9%*|HKXsLLw`zEl{b*eR$EzJiOU_vt2SaZ9k!mn`4 z1QuX2Vj5?;LAq_BeIp9mQd2{%y>5NSfA(dLR&B>Z2X{1y{_^eGhN+<)r^ZhU@55_h zW}|m*b8}QfEr%VQx$ho*`t%BL6=pViDSC`Ouh}@DD1CLOls+qYF?%*OgHp~edDD0a z?|Gdu-umhzaq`7_mN`7 z(O%YBYp|#nwZhoHE>^oP$$Rg`fyw)9Y0GK32saTs@pRuNoy~POzRB8QNU_61an7@b z>7Ew*@aWUGX7Y`*Q#MPtDo?lXM>Ni_m2?}Ao$k0iKNpKS3cd!I5HRfDO}=YHo6jcI zblYW?`^yJ?g#E>Ol!L-u(p_U>3F)NU-I}ke-X~mb79_fUvVdm2n_Vhnq5S-066a`_ z*{gZ$KxsJ`7w@3*9_!9Vy*t?*(U0}7k zoesS=;1CEoDX8eY<;z)Ycpp=w14tvqQUkor((RY`Fv%ReIHvJM*QivycqtQn1nF67 z_aDQ=BJoSJQG)w|u$yR|FT)34G&)aoY#)VbQtVzVO&6>?E`DK@Qp)Ajx^)D637B`+ zG1bq{E=LM3$_(vE{T5=RzMi^4T7gD7WQge=z zhBUweO!$aw;vTDiAft1^(q6~<@WS82CD_F)w3!~;2QgUI&T(iR+c5!`Rg$z)gRDW^ zD_=3}+Ikp1T)38EuqS=kaQVZfY0vx7{%WiEJYzv)gb}-Z5&H?kg&Gk@dBVp%y4AsBVI&8$e_Rgk*Vx@;_ zwM8ll`M@hcKzL44Civpcx5RA~^I+pOvGp3WbxxUHcLjUOq~h zf{gvVBNV(CWrT8DB#4?k^46{&f>s&Fe4d?TVz3}T-djGXRw7~7H1^HixXHTe;I z0u_ByUA}3vCSKEVaytX>H8`@fsd#9wks=&#Nn&8fA?_eo3JfQGn4Y46#XbZKdTx%p zf|b&V|21Fcf+j-&U;?J#ul=o>_4N=}p4k%t()Rk3n#D6uRrxRSWiHh$iv`#rPCZ1i z<=Y|d29@N+Lhvhn@N1&sPYXc;r{%YW;8&Wg?0-qkLZ9)vpviuxX4&DqzErbRHjsZ% zv+ibLW_5XEKB80d#q9^{`w`v_=oBI`{evd^Ma_aO1msdwt+eoxLKid{wWPoo`W2`s zzN-RM{G29Zk?Z|VlWD33s9hZ5E@(1VCxahpvR~a}`S*;L-)`iJLCm;TpMIsu3f3)r zSU1fyhz&N@vtraNHnJ1i5A}0WI1qdvp*x+^2jafQ`FX$42UU~jhq%jy;3qZf&xg3b zp~>E!AL8EstY&?s$*j!MqCi$l`ABqV)J&98Y43wk?wq^FBqq$d2>&xRYks%uHuA#t z5n(;``=iTPVdpg25Lt(uM6-1szm)YsbCQni0zEE6@xjzn~XhK;*c%NYhyR;9QH z{ZuF&mw0|1>buW@ZFfrT_fcn1^7dU1f=)lqJHIFX5E$tBK?LY`>JNq^S-qE!#$Q%_L%(UkM1$F{PCt_g>4@lODXhwFq(|jkM#Cu z)DLIgmo(YmAL4{Z4Rq0rq~qv*rOD10g3-8Yetci_^F%{R;pT4(0bEVT`5{h$OPAc- zyO(!~9vWel1)kC9g?vpk+yHE)H?g%9EdRes_q&RnKhCe|EIN9%Z6vauX|FrQ_k(g#dPlllkZn zF2bCbRFOa8F93TV?jdX;_(qfcafn0Dmz0ot5ta12L)<37!Rcq)05)J0p5)&YTK46f zzfnB?7W4Y?^71bfj|CX!^*>u^&us%!`6f-rd(2*G|2?*WAERUc_psfXAW|Zm|3vY) zEVQNtUlotP#=QPbp)~|Us?cc#w|!A`3b*AIt^SPd{!O9%6Sf5 zE-z(0tlt!mwUSTY6pzRGgzW^jzhb+&=JK|u-!QM=7TOD&?q3vIL0iqjKViGQ$q|89 z0l2?cJY*=J&F6fNjy=bAmC)pPV739fxCXMX*lzq+Z1*>Xb~qPL?N2t{uh{Mgl>&5_ zDBKU8$cK2DfY*B;`j9ULX4B2P#s>@X@(7|sJ1C(l6 zR^Nxh0tC^YEKdK0wdxn7Ab&0O@73A=(pvRZo&D#bP+Ey)chYhl|KpEW;=ha-_;an| zB^3Mj>g*?f!54~8dL1AY4s7YHr}UaGUd z)~dgZ7;vjEYWiKR`sav&%c1aT^~BQmh=K0tJMQyudmh)|*F9za-B9>T#K32aQz{rz z@QcOiD^hTICH?^^__I0-ix`-q)tVOeryyoxpbbsheD8+|CbrX%6D7g1UTdL)MGR;R z8jAVn{fHF&5;5>C=K5zO^?rUv*N+4|G5{O!@e29BDmUjlsF&xTcF?~enCy!#Q}usW zZty_*7a6BtMXCRHchGY+m5};YZt7Ac@h=H@Sh>*=6IQ^bh`ZQ96G;4F zi6%=S++lD(r0W0c(MXN5?dL?3`WLkSxf$g5<>q9H9+y)88v(C-mMbPyAS^0gRQE-# zP=tB)dv|Kg=LjcG$Pi6HOA+rS_jm2D`AMOFHG|l|QQZ1y#xwN09rWkXNIon6#AxH;MWP9H=QB8|jmJoi z*Zlj18&On1Ug&)(Yo^tC+cdBp)G9fYs;d=^5n_S3uOB7356unwdq<_nOj+jlL=&{A zvVS&%90DnSlW6i|xoJX2-20kn5{ip}L{%@CL5Joqv$Beo^(RqE6qTCun3BjEdY))9 zgH3Ufs{hZ9O0?CUk1$c{uL$_02FoOb^1dOO^85=ki278Uong24sthdAq;J^iT$BpU zHM;!*1Eh5(K(%!R#S>VO13FVmAD{fV zRcpZE1N`9P;Q%OS07R4@9%O-1di(#2@RV+_rS|MAbS1I+aN!%JAzaF_r_xThB%+*lMs$^Yrpf$v7L|5Ijm z1bnk^GyDHtA6%{f$`7thHrBb33?6ELN%FnV!vUD};BWz&fLj0niaX%Sk9}StJ?4L; z&p&}5CC2>fqXYn$&#v5ohXcUDz8?et+}DDB1@NE#0qko5%w@3OBy?D&Cjf;Sa0MX{ z_Ih!v56YD?sf+wKY;vg-$w*310Ks*&n|W9Uf?pAe|NLM2Pru?Xe;S1U_{aR4zY3OT ziUlACP+e*Na|`^7{5w=COtvV4J}8E~y-H9;_Q200Ts|k9d?d2w7h{waqtV3M0Y8m! z7P0`f$_oV=##43#$1*tn?`;2SDURp)C4cagzePc`oz~7RsjwGLenLUS&ce_0Oa3FC z@^SXqd7dehOiTe@0#WkEJkvQ!JE$Kl|L&<60^9TphFuZ(I?+TG5 zG@K8XXDWIVgqx)$K&!eE@uxh~4Ltu#1-gLO_}4sB1Shl9WUF&wR;yH{lf^ysH#?Zx%~^|nExivl+|J?6_#gu zA&m6rp#I2H{%QMXX3hism}mNJ`RDS4$<(XPFo{@@W^j^PyOLz1aVdQbL z)6q&Ps>ktbN+`m~`mN`WTqpiUf&S&#_%*-e-yItbYQn()Vpx6&AM?~2%#!+Rp6PFd zk&9#FcT1}NZ}UvQZT}XQ7jNqQKF{5HdBtPd+?*E3a^^ZDf347RQZ{Thp@+e)=DFuJ$rQa`IJuI z6Jh5bg(osbg1#3DkIOYo6bRlcUzh}AAg)03I^G}Nj^5Ls$cEl9fclX}MgVm}<;p=>YcVedY@%rgqTIV(w}0->l$)!QsbUJM{O_vJuXYhFjAGZNDHD5*i- z#42bd(!?oib;rjnr2CRaAwhaqV3avgye$eKEk~#B1MGjVt}96JiE~XNU8+mVc`9g5 zHYL2|3NSJn%AaGF5h_)aml?K;ypa{9=5{SRUTgMRmiHS?dS9(5<2=BFC`QJo^ojh? z0!8K%hD%DQJpUcDav?72~{9-Y0xW!KPx>Zu^2x2X&`jhs?q*yg*VlFtar$|2Wi?sv)G z7~k%?hI-Rkl3BOa0iGGyO;o3qaE?F>tSFey zCUjH!tm$y{%HtIF{3{zdl@kwV84s62pU3VRD2k?w(Ha_Oh^=HSfL=}va@?&O7O-w| z7;(mzhPoE)%pGkOIaG5=DH=zK(Hiv1xzmm@a+QktZ@(;=ctFSXU}7<8_T)ovVHbBP zK=iijn+%22AbOW~Pde!<<-mB@MxpJ&w8Vy`$Az@zz*6hR%Hl80DReN(oD!mlxzHr8 z=(?>b%JJ!~mMA=&-eeT~4KS3=N6Y{ z0DSW*j%J|n7g~bmfel$*r66IZjI@FTJ9!*MHpLaS1WKjPL;N=60!Ft; z3RSdBd~qRvS+HQJmW(H@Bs&N`u1FicE%U?2z|Pwzk<9(I zdQl4OLeUMd7FBDG$g)GkSb7Sp*8UiC86#XMx>BeX|CAUJSrfQYuQ$Jh}g<8ahueo_-c2 zj_-g9%Xp*hNefcDsQ?HyPDhVaMYY+fOq-ACq@AcknmcXEImMD}h8%L5-j)+1i6@Ch$YP{r0@2Wgov z7G<*07iZD*DMAf#Dme1+C53wY@uR~Hz&?leS{1uw2$_$G7}N+F8zj@p6&^Isy|oCq zGY_{aX+Q3*S5+q6zBwLF{`T$17nQAdsf2PB7K`w~AH)$Yc;KT(;AxY)D-F&T{Dpu< zMK%QY1bVSJQhoQO3#<|tc^4G80s5-;ES#liWVJT}Pw%SWXG||>#oNiC$sm}Tu!TY$ zY8PbX95dY)c8O`AvnM;Er)hCXS{PaH#+0K&_aD3c6q?$M_^&E=5 zdxoJwy?Cq4J2_i*mzA&$?opZVDc38Y`${wZFk4Ccv^M%QZXyync0zJsp4MRSwR8zW2q864&WD!skI}gSf5^jVV^E z0;PWX@e>vCU({X&LzVmI1bUx7_tU#ULd>kW!y0JDSU<)(MQzU-_zo>(zC+xsVaj_i zop&2g+*!UX=>aJ3vzf76vKymAY@dCQ0Fn*TL89*)b|j`<6FYNasz@jNC-OsEMF40^ zFBge~GWwq$2G_4zEo`n4=Z-pruA#Gu3_6Yw*IjU^sLhaNOza*ECV=2}y|?6tR2&zU3jjGF(4@T_#cQs%7HS)sL)= zM^fIB8F<_Q1+;yFAhp-AIMsQs9Ol9AZDwOhjazGb3zK~PHyzQkG#0rK2V@7Ax!ASy zc(g^XImm*&NadT&hop;hgrA=tNNBFy-O!lq6%zu|Yv%!~T7%W<6t(P4vPED&zx&fD zJR=33R#}xn9}qZSve#Ir78B?ro_5rJyIAl8$kU|4yHs0-<_sI{I^4VM*iRS%%_m{@ zS%#0aa1=1b4y8sAG3e7MCSD;3y*ipip}*xt(IXr}hqz4v7YcSmu2w_tB8aV((&PeF zrvb6j9%Ivs>(&aQ(L3~UJbLZp)+dUwVB|2>?zW}wje#Xa-9`doa|a?IeyReIwZZ`f z9Rvkt4?w4D&XA_pmrJpa=CIif~69`J4)Zq*4TxY z-q+P7V0VwEOBBhyRtoD#4F&dGVXgRP4$!*F2J1Zy$C)!BM}QlI4QV+|uws|lfX_ea z2(zfd`?;@OW-H=d3dBR$=mC4NsY3AexNQ=}9f(2zkN`1SQl&T{`05JD%(yK)qR9N1 zZM)MgO)l%=DjP{;aJ53BB_7dlVxERgj^LL+muf#g>#2T8L`9|K8 zC9)=Tb-3qh@oIuZ%otC1M&iM7oFNk6UG?3HJds;9aW0zo$P&H%s6)1R+}S&Wh)mtL;I%%yEIiQOVVz)XI!Zb~(g2acM6SirUda3xiVy%ho6_gflI49mLtWV_2 zQAY8Vc`BZWj0LP7!SKu#jHDBE5;ZM3PPP+?d+lpQ%IjWE%j1qGzKR8Ynx*j0OfLzT zqNZjh!QmwlFF4I_>xT~vtGczy9)M6KiFC%N^$y_Kk(r(eJ4Q_S>}q@?*P{1@RPXo$ z2V6@JBcB-~5A{wA&ox<50=?1WG+A*~m)yq$>Q;eRz$~aQ${YFdPRQ`F9<@}4u9Fq6 z)&mD)^e=SO$JI}HHVt+TJ@EQ&CvCN|3aXgR8`1=`Q4)?g6Fj~Q`IHP_03JvJ!_PZ%MN zIbcE?og;^NEMe-F`#?NblQF~J;-12pX5Pp~UWnV>D_3He+Ssro?@@vBBMS^v<8I^R z&!*n1^RF`h3bgKLjuL)QiVCeum`9x3;Hz%wTgpJ3P9}nixviz zo5hrI`jjUrl{q*s3b?l|O27w}S3cYkMJjr4q1VyC_=OLYL4hRci7@A;zu;E-<{;s8 z(P}maw3$|Bm=;z`!<{5d0LO%#pHz$~UdHm#^bI{?A{(e64V3u4+)!VnmJ#cEeQf^(hfbwC3ckzgO z<9-OAM&9RIX~=qfFT9Oay=nqA3+PSgO=<-ax)5@3Y2+2WdEeSC3o5PG;b^10lpB~= zq^mW=o+Jp#gvTfqP{3*d2tDL6U$s;i?A?S9Gz$IKDB_iov|T+8#lsR1rpBx4)RJr+ zlZF{&t1VTt5`~ju5y1YKSY=Z4G!i3DHL$lAPQ)?W=OUbv2rFDt-!Rt8xMD_Y*l7tW zM2}Ub-xtQF0$H0}qu{vffA%Srl8WUvH}#9%24-&r^aE-kAsM+zGk3RIl>E{{W2@FV zv#3eRs7&m6$$JC6p4Pdk0g9m+eWr+!kXsfJBdA7lc-m2AQc5GiXKj+NmkGM|9~N@C zMN48>aR*elV#rs^c+xRBA3W@%vwl^1*O?xO1!xz2bL&bB_QN3Y5Bz4L=XA8)C zb-)YBz|rwy|!4u<=8ctC@1zz`pNn)hpC47)gC6bXBO*-c~TfN+6Cfc_e2c zu6r7R=jyywB(Sc#1~am;>jNleP#bd9qpmzCKeiDuuLp9}{iNhbt(M4vze`FH1an0R zm*+Gv>bPn<$|@?L9*lstM<4_4`GumY6QK%`_mgH)l;Y~V;`6N7jeDhYDa#%>79G%p z_{!piYOV7y6;+et=?nuMhufJR1f{2FBeZIGh_SB-DO%LJlRhEfgmghqOSmGiMbV=QbmD%%GC`H9nt?HRyX|G4dHs`$-;#<3u{NmBN zQJSydi(`d6!^8t)a-hZtR;ZWw8m~I+gHb zhUiUNCkL7#QnhkO8f?NNoPHdZ{W1>A z=Vbh`aRF0;7`>DPN)ID_1O$9=W#{&idUb1;1GFLn6(rN|0N$&fQ@T(R)tf-zB<*gGy z%*rNmGx1|iIWy@S@K=b_G3G;PjC&&VsZR7By+-uRC=zAJ&?ib1>0!|&Nd@jkw7hoK zjEA53I+Q(zKB39v4tNdTu_}^Tj0ZJ z@Wuez3s)*<|c^E9LoV>(Xq0qMI9lU4Eui zKAj|?Y^mzOIRrF#c|%URL(0^sAVEO4RpC*Uw&GpE78baNgzBj)8a_qtv@xtX#g4*w zlB^}=CuRCJSjORbS4aD`Y~nNEaOrFcQAbI{H*?Y8DzDXGaTXTx-L~guU3O;8dq2I( z%~S6CXkF&auGmy}#kB;>X#h(gzhvtGOEtBQq-{0u%k58jtGGr3xCZN(aGO&+uL;WI zBITSVQ#anmShC;7V%u258Qt7QG{qnHaj_CxduY)wiACSjy}RuZ3}|&x|HS3)WTMWP>)s@x;H)YekWC5WL5KVl ziC*#1KHDqtb~f@Aizf0$DtCbg1Sp#jn)_l3@Hg%A=Y{Bi0e0JUE@y9Q_30Dvg*`8<3ND$rU^FmGAXetp)RRw>HN92B)aflA zv9~P8Yvjd1>{V(}%jt=0;>;~V8Fxat z2_7b!x`0T*RzYt>baK%Ig+*b1bh6vNgo;a|!FVjXoe7G|;^Abs9w89DpzscmL`dKQ zg8TE>nI$6aX6y%ZSm=RLR5))|APjFcA}wfWymFrMXO@jo`Hj;utURiSiqXd@RLq=) zNLF;w<+DHlXb4nFZ#60?tmDLz)wZ>2tQT7RNYqQFORq$t-~v32W9XHlBsV_smc8=v zpdP15R6*5)gJggw@)04vEt2@xCuf{dN5OQ_HbGHoJAzFPvQParhz~3#i|;=3C)56D zRbg<3PTMObReg81SvGl}Oy}tSQdig~+T$`V?@?lBx-u4u6YEA6&GK2Q*T;4{rq0gD z*K|L-?7qR0dj;g-e-{Pv!tsb8XEujuahY#e>E7vT{B(MT%q9rO|02KHXP?-tY87-M zcf)&fZYT@snI%R$GV*?@5E}1FnaI^U2<)O*65w)C9O)vaJ8%gdW&%ws(HLMtdEeqY z#I~L65@gpa7-)iCGNDKwVPbQnS32F?kp#yZS3t0)d~EBlP$ywhf>6EfrRdDZ?n_^P zhQKMq&d+Qw!}(lvte6?c+lB&vZ2pBf-&$6sT+Nd9n4nMpN_6k7?a#&1g2*&g3V|z~ zN@oK;1v=wWB0f0#iV(W&DkbUbU0h>lDjs8$g!jg)ctu5Ms>iPiy2ZTfc)Xg9@RV?wt*6PvR+c(^lctd&v1Fc5EFAkO`KJ zqY|T+FuK{>b=-SbqgtLaD!$G4d1mF@!|UDCv(#sJF6@coqg3gS$EaNm?!23r&N%dK zIviAI_a`#d!g&LUR?Naoh*6Y`1YfshNWly$K~aL5*@o4iK0|=^?v}sxxZPU;7LBeu z8|5DerRdWZC#`a+n97#hp2r z1&MrG-v<|e{l?sVOByb}4yUsFegaIN_0+h$cEo~cbWl)=nyUzHpO41!1k2w3LL9Uz zBPARn$rJLrh|aGc3af+&y%8{abAbogY=p}P;>Rdg3KYyETu@^xH(5MZ#`n5 zaP$!Nx@Fok(rDf2$prWvcV`m3?bk0Kvew6o?EpYqg41 z@IHvr3qw|tAPzP~&|_s{ez3COydVuo2O^9xJ&Ko)W=)Zq2ifm#4QIDfpgXJ&*`F3d z+6x;Avqe+H-lz3sHx|ZC#PlIMaOt^WN+}>XBHV^Rfe59cd{}f}ohc{h33!}3a8^iI z)w_;RO@;-CmjVyRD}*#RW5o_nuYu6Y*DEZ=2CQbAHdO()eX-8Ki;^TW>rVhcLfd1x zWRwqI*GVf|GrJjL`T@Ndf)#uzaSiEBX`KwCBvs1%VW&)lEX3}2387+585g{%gky9^IKl|7us1%Q@+giN8+D|LF*Z)3 zD_E0LuLAf4&F|6i@f=}J&Mi2w-{buO8R;5N(JyVVF)Ce0{Q=c&EuQBb%nX?N*q-vj zU&7c5nH2VekrmiShw>iB#*l8q`w3)>_@bN8i^#*GwWNd`^ciH&jA^0-lw`v0V|b_Z zk!urEei+whEFi7R>$sHxKUrlYEboz#>xZ+Xh}-JBZ5gWxm4ph@?GuxBP2EqNhhY#^K9UmnYrPTP*I7`iD zj5*d6q~g!~*rGIObu3QJsgA2r{KGn-r}ibrSV(q)=BANMWL8ylrpY3n{*fn3d0mwL z>^WRAi;c(J-MlxhY&xOs0kkCEyjwp>37 zje@`-zo4Po2fOy0c^t6Rk$@e!D6D3)SL@b2j&YosG-M>^AJ!B|zvyPsHE-$#i%a!%HB{t6kxgCzAboE(H5n( z*U?Cp*vDABV)BBoc5bP%G@B(p!vyi(mAS;3me{SU1*R=`)p|1tI4sy>-DcOmpd3u+ zHpI=_YALYxepD)q)|S2JjuIpYc07FRHGe}TSlqewhIi3f^I{Pb{BqiBu%W)U4!dzU zPT)FtEE~Ln(Uv4_j_bsXXw)+^50C6B;<*4zQQ zSgqfV?Dx{9(+!wkBhiFhA(JdiRYabg6BW?&>>&8Of()Ef>Bmidd4E095EKd?oW4Is zl*j20)w)YOK1ATOe)sxY>BlDr#JhHA<)>@561^7hIGsOc% zF@2U~fdC5N@_W?h{@9(GUq)9~Jb|iNFF;vfEZRzN;1Ek)Q@v6)hctBgsD1WeoT zM4L*wDaw`PcH`aa2&6hcWU2+)kiRyvL}QYd4?1{?+wDM$^_ z3R$PJ!~sAk5CK_~o@0?_B+oD%QA8p1xB9)agzt2E)8{FIDkJltUitKSS>0tU5fzOD zR`nz6%}krYpeVv701*XSHi!z`SC-DA&#(cx0e3_Xhy5g}mneljOVUO7?%4I}UYvC$ z6nhd*to`;7Pk3319DKuqPPc-O(ooZWw4>;@vl0EEm&AyhnjjW>Dqd#l;#N>97ENj! z$e1WFA)0Fam{uf%G%Ct_nS%tHo;eRkl#)g%vP212_Y87$BlT0ev@Z(31vhQ8tk`=+yv)vaM$KFZ79z`pBDNTuzo@gPZfZlz^q@(t* zM5uQL1RBdo8W((Botl?rML04=lGccgArfO93ZbC}Dkxu#bc~Mb#7-Y4O&+g6h4f~R zhff)>%=975)JI4{`<%!4nYFQUgklSdq>jpZec52&?-NCBBHnt*0F>UgQ3=oS{@{@w z`7O4Zw^AAVkbO64(4JrpZr#HB_-rBgn4^JKQFxS*{?z?mwY@$&p}~StR8LXpQ)9wW zD5R>7V^t#wTvqAhDZ{IY)uVHjg|z&+O26YXW51rNHX)_>$iQ;d~%a$s4Jf#Ig!I}Vg`^Y zr51IhU*DA5mKpZ2l8#IqSY;ksBkd=QlzxlWpCLN5FGE-Ye!e(A95Oht0Dd-t@O%eV zD)p`8q80a-&f(1Y;hn~z4UORsXng?gA-IL%;qm9olw6 zl0A@L%^D>b8r}QMR-+Y0WA5e@7g~_qp?YHoaa*iG)WEomPlh1Ti94{KitX3iR8%{$|T+dR( z80t9_9iizOHqex(xH~L&Dyt-`ieN3hk10pNqKfG^@$jak#A!Q{qO2+BNO;vS($L5m zm;7mys=ZJ@rkJAg10};-iU?x;$U_Q9&BK5pi5KGvPm3n>vwPR}I}E!f`8lqC@tr{0 zR?tD@)iqJvOi|g&dI4#E;c_}$;;a7HN*atf8e6We=BKD~pzb|BAv4rZ*P-Y#r26oU zqSpf@wTYs=z$x#Qewyl#GgXtR>u6YU87n-v%zB8 zw0_#T3oPmAZOh4MO$Se!x(3~S;#(%PUz?HP!Oag7>a@_H4BiB`-U{kC)ou~f>A{G<2RhHP=OQ`f}8T?S->f6SDXF7G8=0Urh`xHuo%Z>pECzzd_Vp@zY&kdAWA?nk^CQy|0a*trApF3Z*T{19 zXf6GDLXrjys0#c&SID2~_-I@o5XSX?WnWy)WQ0QhVI&}Jz+ci}2qB1Ad_cYfg@sEW z`{Uv@q$RaccJ5JwFi-KKn80^NVbrWkfeK6T7>3Zo?yKUc@6mxV>HHYOC|WqEOlqvx z!_$xxGHH==|N>wz2 znJ95OvdZMibJe4V1cb|>^KeWAtTa^3SzYQZG9YY9+QUlD;AdM*C5JPu&cS$Qqcg)b; z-Cd#r0@B?oNVifF3eIoTwbtJI+41b>J&yN(xR1~Ez2ZJky0n1EIK~LsiHb1!93Hz+ z)cr-}!1kVa0WWKeYhEWH3avyI5TvzLU$!3RnQZ=a8!7bwj568098T}Dh4(W=^k>Ad z@n(X{LJ94Jv}9={8N{Y!S-RTr(CDnCLfWw2M`Rd?nsCOs=j>2s#vBZ?nzw8RmQ@2H z-{@;Zy28kn*$t+(^i}K=XdKvIc!+PI-?_!3f_~u1r8(0UsDhhcQ7}Pl z%Oswsx4`n=?e)kvd26$xpnB)*U3xA?ikUfQ?BMPFxd5<`D*^cE3+K_%ecY$@cr#5sURzjiB`_=Vlg>LK zvA%q1IcW+&jPwqO+#;?jIsMKcgyG`I)Akff$Uv{0y!MA>M3e^U4&~0iXZVWP1;_O|9)BTlt#z^E>X!Nc-&= zV6RJ}*xW#hy8?DG6&}-&qr5hE@!X^HIf2B*bBqZmjv;5ib2FB6LNN-%)E3r0W5l3p zQ>QBF%~-_5D8c)(|k?|NR8Rw3CY@1%X!ou$fA(~DKs zR6bF=mk=L64%Q@RvQ*SQoH_X5owyzAM6N8CxD%b*pyztN8Zu#-yNcWxMCYh8d$pK{ zRMErIvloubweV0cAA$uR$8k?* zAMY2l3bb|22H!!&+H4;aHE*=az7-UBu|K;M{-_{1>+aQj@tR8`IeSqPOXDHa zb!hJBlhE-#E^i^JnZQ!M>hWhUkDo@wUg;=~!%JtJx4)`|Us3v{Y1b}PDu2uN^H9D~ zDAB%|Lr!|!QC%GPI?Pp}C7=T!*GBS-$!#Z{<%CSUd!- zi|~~SlFw^{<;Pml55Y!1rFBoA8=24J+RfYZhce$cwr6Tf7n)ng+QY{~ZhSojXFT7? zN5)gWXe>i9E@0Acx%*}co@f4C znnvlAVP>k;Tx{UJWNb$Z_ZqS{>dphFgG-)`fdm$!UPyg06wPTIz4q!!Nk_;aW0>Go8q zdacuqh-ZG`1G_EA%i@EiHq*DyKV~yu25yhPw-}%C8#&@z6>ZJBdGzD@+r_EZ(tAte zoOVqrW56C+HWV8ulxK>sRFtRWaTrQr`bbkJ+waDUe-40DK#iF9n;}=VH)-D+a)7h0#{cDBg3J7kTR1dy@&!HY~3UzW4xJA`*EaDidb&|MjLc6RwWEpu>)4u!(84EGPBH!7Y z@|o#i`Qg+e(queW)Tr5;)m?2bn8RvOt?HTHC+Q$+HW)@`&rD5}Y)k>TbW%XG_SMzi zOD=z59~?TMOpS{E$CPdGMM_LQSY?@A1XcO35P0B)FSVGb(tkP>E^6e-hLx8ob zkCA~6U9?bqh)V3D1=7nV9P>oTDR@}d^kY^SQ6jW+@xTm@&@V}mk; zrGaW2u~1+wtXqyvEhY_vEz>4{5UdJ+1q15U6@-#iZ;TSMktDhuh$CXAnjAjeGO(j5qRF7@LnAhxlvbyN>3 zA>Fbm*BytU+T`W|tOoDtlIg`vTTE8eONvpcK_v*6NggU)&Nf%RdG!5ocyV2(FCnNb zK_w>0n+nOqPW)U+7A-%tcL3TjAR%W|_+)yecb+fA79E?s5Yai}DNGeF z0($<%hxG1b{|79%XX5&jSe-9*U4YM_TPCsnh!HWgOgvN+?$}d z0+;G&Lmc|6KA0M>#+n&D6<zu zNTWbP7D0>*G*BH4FxDAwZCDG^t~r=yG;1@}jV_@;1d=+o7np+cF`Un^|8A-(jOFOEK6`BOIY!wDPa)wd5<<^Jpl~!W*fpQ{A_pXqj!Ux^5 zlFQ#nTb(eNYU8n4n0pyc4tL!JuHMFpEW3DBCn0E`IhT^sYDuL~O!V4&X0o>ebB+Oc?-6S!m+iK@$4)FBpKyq%280E8U!&d4+}uf{s}w$Y zN>s(PvfMVrQWfQa==*r$d8bfcAoAJZUF~E4#ZHhdm(!Sj&e3Zqh+(oOV^*~2n_l}E zOYcmynfem?-88G=!_)YythJgnrgztdPt6ohw{J5Ew2xxK%}EtEZzdMUQap3RSPIhL zgy`@&gpp*5ifxSnbozO;cp_kjpHfR~XKKFU+7hrY3b^Wxz26{-<*MF?^-~PqKlXs% zib-a`yJ*Zm=d9a&a$v%CH5pkm0{7xLwB=$lNKK#FrYm)X@RfVixgxo|+A{6Vb6UGC zq)*e{iaDLWo*mI28)>vG_}Bp`pPG3s_@N>JP0?UqCAeo!9G#Rd|2nB*hu1q9*Z*k6 z$TC;*D?Id=<2ViSpap<87jGst?zp~kvt>z^TR&;R0gi%k4)l~-gNe~$y}lASOX&)r!1jy<(YmpW zvysMS$O}ZOdHFYycd0lSLVJ!XG~uviK2Nq+iuk%Zi@nTz%$1&>^&RcqaPGar`qK8$ z)sb&$09^W`rNaKzkzYf(OxdRM3p^uM$wUAR(EHj$oWtF#b?~*uj z<367C1j0G{E0nQj-l%IE|)yHWP&DIJOiM2m+DZf=N<|U&BM%W1T z?mgn9NQmaxWLEAt8hrd37rQ1#v2Gtl zZH(*~epLTM5(zuWG%#JiVQhlNM4Jt|a11Ol$vD9TZ!yi}gGsiC|At@HD7|e@1s@KT zio_k8Knw?J=<~wMBjfWC1W0y-iFItdC}EyVz6BI;*&B&`PH2l`ws5Ta6?X6IW}cL7 z;EZke6to*DKBS&7uzIQX5*!0)NZ=A4bj&w@<;o=MU+jJ`O?ZbRMA$bV1)Pe!^-3Aj`^9yD>DJ zB(US&vg6s}9o1IE$IEX9S?XD78qON~Be#UD;z1S?ppE-BIm~w7q$8^6p7lmdk210L zmq5iLB}0jwC*}(I+I15^&Va7z@_8^yfjd$OtEcKNd@+J zGUOR6g^B}Mm7PM>duODIAvDECxelggyOji1xBgrjD`XR0mZG|`ZFp!Mr6v=QQ5J{Q z7n_3}FPRy^8WzN-``m3d?tZz7H*))V43+PojDL$<&M4F!btLC-+v|8+xmQ~>+K6$9E)NKnXh`DK4!e)3#6eKq+ytEl2 zuv?;11_>Dpv46FsD^%*}y`a&X(Kk5P=vQuj4u#9H_LKc@dlT%xbkxotoG&x&O-WhIj7>E^u&tT0_HV~b%-1o!N_|mp>-}}^ z1)?&8@J^6~u1+MY5#>@X2~yqGg*vK!AZefy&Ow>yW|uo#nQ6rm>B*L0m4OQxZ#`lw zH(l(hVe87E$ec`xbfv7ghhgs#p3!nAiPyH=@}Sx$RX&eK^V;rhKjz?TJ*N?!ib1^y zVd*#^d*Cu5)@wbjHnsqd&=;Y4qri;UOAao{_Ic!b+D*7Xn|=!zRrs7;87D`%6YKPl z-5`2;VmeK|m^7lt!6yn789~$!86;>{E9>6COs7jKF}xc(-yb*MCYt5h!#B5$-Y z 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 P4IqQ56OX{oM=T9{l(`R{R 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