From 05ec0285924d41027ea6fc1ed6c3eabd15ce9178 Mon Sep 17 00:00:00 2001 From: Vadim Mostovoy Date: Mon, 8 Sep 2025 00:27:14 +0700 Subject: [PATCH 01/18] SnapshotImpl inline class optimization --- .../src/main/kotlin/ru/nucodelabs/kfx/snapshot/Snapshot.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kfx-utils/src/main/kotlin/ru/nucodelabs/kfx/snapshot/Snapshot.kt b/kfx-utils/src/main/kotlin/ru/nucodelabs/kfx/snapshot/Snapshot.kt index aa7bea3d..1974e346 100644 --- a/kfx-utils/src/main/kotlin/ru/nucodelabs/kfx/snapshot/Snapshot.kt +++ b/kfx-utils/src/main/kotlin/ru/nucodelabs/kfx/snapshot/Snapshot.kt @@ -15,7 +15,8 @@ interface Snapshot { } } - private data class SnapshotImpl(override val value: T) : Snapshot + @JvmInline + private value class SnapshotImpl(override val value: T) : Snapshot } fun snapshotOf(value: T) = Snapshot.of(value) \ No newline at end of file From f8726a77d0f63264d6a391020a83e2f68f8443ad Mon Sep 17 00:00:00 2001 From: Vadim Mostovoy Date: Mon, 8 Sep 2025 00:28:05 +0700 Subject: [PATCH 02/18] Remove std package in common-utils --- .../src/main/kotlin/ru/nucodelabs/util/{std => }/Lists.kt | 2 +- .../src/main/kotlin/ru/nucodelabs/util/{std => }/Math.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename common-utils/src/main/kotlin/ru/nucodelabs/util/{std => }/Lists.kt (90%) rename common-utils/src/main/kotlin/ru/nucodelabs/util/{std => }/Math.kt (95%) diff --git a/common-utils/src/main/kotlin/ru/nucodelabs/util/std/Lists.kt b/common-utils/src/main/kotlin/ru/nucodelabs/util/Lists.kt similarity index 90% rename from common-utils/src/main/kotlin/ru/nucodelabs/util/std/Lists.kt rename to common-utils/src/main/kotlin/ru/nucodelabs/util/Lists.kt index 94fd0597..ff272409 100644 --- a/common-utils/src/main/kotlin/ru/nucodelabs/util/std/Lists.kt +++ b/common-utils/src/main/kotlin/ru/nucodelabs/util/Lists.kt @@ -1,4 +1,4 @@ -package ru.nucodelabs.util.std +package ru.nucodelabs.util fun MutableList.swap(index1: Int, index2: Int) { val tmp = this[index1] diff --git a/common-utils/src/main/kotlin/ru/nucodelabs/util/std/Math.kt b/common-utils/src/main/kotlin/ru/nucodelabs/util/Math.kt similarity index 95% rename from common-utils/src/main/kotlin/ru/nucodelabs/util/std/Math.kt rename to common-utils/src/main/kotlin/ru/nucodelabs/util/Math.kt index ce66923e..234b34e5 100644 --- a/common-utils/src/main/kotlin/ru/nucodelabs/util/std/Math.kt +++ b/common-utils/src/main/kotlin/ru/nucodelabs/util/Math.kt @@ -1,4 +1,4 @@ -package ru.nucodelabs.util.std +package ru.nucodelabs.util import java.text.DecimalFormat import kotlin.math.pow From 951315a6793a6ceeff1fa12a924e9083b761d883 Mon Sep 17 00:00:00 2001 From: Vadim Mostovoy Date: Mon, 8 Sep 2025 00:28:56 +0700 Subject: [PATCH 03/18] Add logging in AlertsFactory.kt --- .../java/ru/nucodelabs/gem/view/AlertsFactory.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/ru/nucodelabs/gem/view/AlertsFactory.kt b/src/main/java/ru/nucodelabs/gem/view/AlertsFactory.kt index 691a4309..17471a3c 100644 --- a/src/main/java/ru/nucodelabs/gem/view/AlertsFactory.kt +++ b/src/main/java/ru/nucodelabs/gem/view/AlertsFactory.kt @@ -4,12 +4,15 @@ import jakarta.inject.Inject import jakarta.validation.ConstraintViolation import javafx.scene.control.Alert import javafx.stage.Stage +import ru.nucodelabs.gem.config.slf4j import ru.nucodelabs.kfx.ext.currentWindow import ru.nucodelabs.kfx.ext.get import java.util.* class AlertsFactory @Inject constructor(private val uiProperties: ResourceBundle) { + val log = slf4j(this) + fun simpleAlert( title: String = uiProperties["error"], headerText: String = "", @@ -26,7 +29,7 @@ class AlertsFactory @Inject constructor(private val uiProperties: ResourceBundle title = uiProperties["error"] initOwner(currentWindow()) headerText = "Сохраните важные данные и перезапустите программу" - } + }.also { log.warn("Uncaught exception alert", e) } @JvmOverloads fun simpleExceptionAlert(e: Throwable, owner: Stage? = currentWindow()): Alert = @@ -34,7 +37,7 @@ class AlertsFactory @Inject constructor(private val uiProperties: ResourceBundle title = uiProperties["error"] headerText = title initOwner(owner) - } + }.also { log.warn("Simple exception alert", e) } @JvmOverloads fun unsatisfiedLinkErrorAlert(e: UnsatisfiedLinkError, owner: Stage? = currentWindow()): Alert = @@ -42,20 +45,20 @@ class AlertsFactory @Inject constructor(private val uiProperties: ResourceBundle title = uiProperties["noLib"] headerText = uiProperties["unableToDrawChart"] initOwner(owner) - } + }.also { log.warn("Unsatisfied link error", e) } @JvmOverloads fun incorrectFileAlert(e: Exception, owner: Stage? = currentWindow()): Alert = simpleExceptionAlert(e, owner).apply { headerText = uiProperties["fileError"] - } + }.also { log.warn("Incorrect file alert", e) } @JvmOverloads fun violationsAlert(violations: Set>, owner: Stage? = currentWindow()): Alert { val message = violations.joinToString("\n") { it.message } return Alert(Alert.AlertType.ERROR, message).apply { initOwner(owner) - } + }.also { log.warn("Validation violations alert: $message") } } } \ No newline at end of file From b52c06c9bb51791354e020f1a1d0f022698af149 Mon Sep 17 00:00:00 2001 From: Vadim Mostovoy Date: Mon, 8 Sep 2025 00:35:04 +0700 Subject: [PATCH 04/18] Picket.kt refactor. Add new utils: Result.kt. Validation.kt. Equals.kt with JMH benchmark. ModelTableController.kt fix NPE and limit layers count. --- build.gradle | 10 +- .../main/kotlin/ru/nucodelabs/util/Equals.kt | 35 +++ .../main/kotlin/ru/nucodelabs/util/Result.kt | 25 ++ .../kotlin/ru/nucodelabs/util/Validation.kt | 9 + .../kotlin/ru/nucodelabs/util/EqualsTest.kt | 81 +++++++ .../ru/nucodelabs/equals/EqualsBenchmark.kt | 50 ++++ .../gem/file/dto/mapper/DtoMapper.java | 14 +- .../gem/view/color/ColorPalette.java | 2 +- .../view/control/chart/log/LogarithmicAxis.kt | 10 +- .../log/LogarithmicChartNavigationSupport.kt | 2 +- .../chart/log/PseudoLogarithmicAxis.kt | 8 +- .../main/AnisotropyMainViewController.kt | 2 +- .../controller/charts/ColorAxisController.kt | 2 +- .../controller/charts/VesCurvesController.kt | 2 +- ...InitialModelConfigurationViewController.kt | 4 +- .../controller/main/PicketsBarController.kt | 2 +- .../tables/ExperimentalTableController.kt | 6 +- .../controller/tables/ModelTableController.kt | 27 ++- .../SquareDiffErrorAwareTargetFunction.kt | 2 +- src/main/java/ru/nucodelabs/geo/ves/Picket.kt | 217 +++++++++++++++--- .../geo/ves/calc/ExperimentalDataFunctions.kt | 6 +- .../nucodelabs/geo/ves/calc/Normalization.kt | 2 +- .../calc/error/SchlumbergerErrorFunctions.kt | 4 +- .../ves/calc/interpolation/Interpolator.kt | 2 +- .../calc/interpolation/SmartInterpolator.kt | 2 +- .../geo/ves/ExperimentalDataTest.kt | 4 +- 26 files changed, 452 insertions(+), 78 deletions(-) create mode 100644 common-utils/src/main/kotlin/ru/nucodelabs/util/Equals.kt create mode 100644 common-utils/src/main/kotlin/ru/nucodelabs/util/Result.kt create mode 100644 common-utils/src/main/kotlin/ru/nucodelabs/util/Validation.kt create mode 100644 common-utils/src/test/kotlin/ru/nucodelabs/util/EqualsTest.kt create mode 100644 src/jmh/kotlin/ru/nucodelabs/equals/EqualsBenchmark.kt diff --git a/build.gradle b/build.gradle index 036b8cf6..148668dd 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,7 @@ plugins { id "org.jetbrains.kotlin.jvm" version "$kotlinVersion" id "org.openjfx.javafxplugin" version "0.1.0" id "org.jetbrains.dokka" version "2.0.0" + id "me.champeau.jmh" version "0.7.3" } group "ru.nucodelabs" @@ -100,6 +101,7 @@ dependencies { "$projectDir/lib/${System.mapLibraryName("MathVES")}" ) + /* Slf4j Logging */ implementation("org.slf4j:slf4j-api:${slf4jVersion}") implementation("ch.qos.logback:logback-classic:$logbackVersion") implementation project(":app-logback-appender") @@ -114,11 +116,8 @@ dependencies { implementation "org.codehaus.groovy:groovy-jsr223:3.0.25" implementation group: "com.fasterxml.jackson.core", name: "jackson-databind", version: jacksonVersion - implementation group: "org.apache.commons", name: "commons-math3", version: "3.6.1" - implementation group: "com.google.inject", name: "guice", version: guiceVersion implementation "com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion" - implementation "org.apache.commons:commons-collections4:4.4" /* Math Libraries */ implementation "org.glassfish.expressly:expressly:5.0.0" @@ -127,6 +126,7 @@ dependencies { implementation "org.bytedeco:javacpp:1.5.12:$platform" implementation "org.bytedeco:arpack-ng:3.9.1-1.5.12:$platform" implementation "org.bytedeco:arpack-ng:3.9.1-1.5.12" + implementation group: "org.apache.commons", name: "commons-math3", version: "3.6.1" /* Test Dependencies */ testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" @@ -134,6 +134,8 @@ dependencies { testAnnotationProcessor "org.mapstruct:mapstruct-processor:$mapstructVersion" testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlinTestVersion" testImplementation "org.openjfx:javafx:${javafx.version}" + + jmh "org.apache.commons:commons-lang3:3.18.0" } /* Setup Runtime */ @@ -164,7 +166,7 @@ tasks.register("cleanRunDir", Delete) { delete fileTree(runDir) { exclude "err-trace*.txt" } } -tasks.withType(JavaExec).configureEach { +run { dependsOn copyClrToRunDir, copyDataToRunDir, copyLibsToRunDir jvmArgs = moduleExportsJvmArgs systemProperty("app.gem.log.stdout", true) diff --git a/common-utils/src/main/kotlin/ru/nucodelabs/util/Equals.kt b/common-utils/src/main/kotlin/ru/nucodelabs/util/Equals.kt new file mode 100644 index 00000000..1585309b --- /dev/null +++ b/common-utils/src/main/kotlin/ru/nucodelabs/util/Equals.kt @@ -0,0 +1,35 @@ +package ru.nucodelabs.util + +import java.util.Objects +import kotlin.reflect.KClass + +class Equals( + val thisRef: T, + val other: Any? +) { + var isEqual = thisRef === other || other != null && thisRef::class == other::class + + inline fun by(getter: (o: T) -> Any?): Equals { + if (!isEqual) return this + @Suppress("unchecked_cast") + isEqual = isEqual && getter(thisRef) == getter(other as T) + return this + } + + inline fun and(condition: (it: T, other: T) -> Boolean): Equals { + if (!isEqual) return this + @Suppress("unchecked_cast") + isEqual = isEqual && condition(thisRef, other as T) + return this + } + + inline fun and(condition: () -> Boolean): Equals { + if (!isEqual) return this + isEqual = isEqual && condition() + return this + } +} + +fun hash(vararg values: Any?): Int { + return Objects.hash(*values) +} \ No newline at end of file diff --git a/common-utils/src/main/kotlin/ru/nucodelabs/util/Result.kt b/common-utils/src/main/kotlin/ru/nucodelabs/util/Result.kt new file mode 100644 index 00000000..5508df3e --- /dev/null +++ b/common-utils/src/main/kotlin/ru/nucodelabs/util/Result.kt @@ -0,0 +1,25 @@ +package ru.nucodelabs.util + +sealed interface Result + +class Ok(val value: T) : Result + +class Err(val error: E) : Result + +fun T.toOkResult(): Result = Ok(this) + +inline fun Result.okOrThrow( + mapError: (E) -> Throwable = { IllegalStateException(it.toString()) } +): T { + return when (this) { + is Ok -> value + is Err -> throw mapError(error) + } +} + +inline fun Result.onError(onError: (E) -> Unit) { + when (this) { + is Err -> onError(error) + else -> return + } +} \ No newline at end of file diff --git a/common-utils/src/main/kotlin/ru/nucodelabs/util/Validation.kt b/common-utils/src/main/kotlin/ru/nucodelabs/util/Validation.kt new file mode 100644 index 00000000..cc70b50a --- /dev/null +++ b/common-utils/src/main/kotlin/ru/nucodelabs/util/Validation.kt @@ -0,0 +1,9 @@ +package ru.nucodelabs.util + +/** + * Return null if valid else error object + */ +inline fun validate(condition: Boolean, lazyError: () -> T): T? { + if (condition) return null + return lazyError() +} \ No newline at end of file diff --git a/common-utils/src/test/kotlin/ru/nucodelabs/util/EqualsTest.kt b/common-utils/src/test/kotlin/ru/nucodelabs/util/EqualsTest.kt new file mode 100644 index 00000000..6ee8e672 --- /dev/null +++ b/common-utils/src/test/kotlin/ru/nucodelabs/util/EqualsTest.kt @@ -0,0 +1,81 @@ +package ru.nucodelabs.util + +import org.junit.jupiter.api.Test + +class EqualsTest { + class Some( + val a: Int, + val b: String, + val c: Double, + val d: List + ) + + var objA: Some = Some(1, "12345", 1234.5678, listOf("Some", "String", "Content")) + var objB: Some = Some(1, "12345", 1234.5678, listOf("Some", "String", "Content")) + + @Test + fun isEqual() { + assert(objA !== objB) + assert(objA != objB) + val result = Equals(objA, objB) + .by { it.a } + .by { it.b } + .by { it.c } + .by { it.d } + .isEqual + assert(result) + } + + @Test + fun isEqual_same() { + val result = Equals(objA, objB) + .by { it.a } + .by { it.b } + .by { it.c } + .by { it.d } + .isEqual + assert(result) + } + + @Test + fun isEqual_not() { + objB = Some(2, "123", 123.32, emptyList()) + assert(objA !== objB) + assert(objA != objB) + val result = Equals(objA, objB) + .by { it.a } + .by { it.b } + .by { it.c } + .by { it.d } + .isEqual + assert(!result) + } + + @Test + fun and() { + val a = doubleArrayOf(.1, .2, .3) + val b = doubleArrayOf(.1, .2, .3) + val result = Equals(objA, objB) + .by { it.a } + .by { it.b } + .by { it.c } + .by { it.d } + .and { a contentEquals b } + .isEqual + assert(result) + } + + @Test + fun and_false() { + val a = doubleArrayOf(.1, .2, .3) + val b = doubleArrayOf(.1, .2, .3, .4) + val result = Equals(objA, objB) + .by { it.a } + .by { it.b } + .by { it.c } + .by { it.d } + .and { a contentEquals b } + .isEqual + assert(!result) + } +} \ No newline at end of file diff --git a/src/jmh/kotlin/ru/nucodelabs/equals/EqualsBenchmark.kt b/src/jmh/kotlin/ru/nucodelabs/equals/EqualsBenchmark.kt new file mode 100644 index 00000000..f8a1e79b --- /dev/null +++ b/src/jmh/kotlin/ru/nucodelabs/equals/EqualsBenchmark.kt @@ -0,0 +1,50 @@ +package ru.nucodelabs.equals + +import org.apache.commons.lang3.builder.EqualsBuilder +import org.openjdk.jmh.annotations.* +import ru.nucodelabs.util.Equals + +@Suppress("unused") +@Warmup(iterations = 0) +@Measurement(iterations = 2) +@State(Scope.Thread) +open class EqualsBenchmark { + class Some( + val a: Int, + val b: String, + val c: Double, + val d: List, + val e: DoubleArray + ) + + lateinit var objA: Some + lateinit var objB: Some + + @Setup + fun setup() { + objA = Some(1, "12345", 1234.5678, listOf("Some", "String", "Content"), doubleArrayOf(.1, .2, .3)) + objB = Some(1, "12345", 1234.5678, listOf("Some", "String", "Content"), doubleArrayOf(.1, .2, .3)) + } + + @Benchmark + fun commonUtilsEqualsKt(): Boolean { + return Equals(objA, objB) + .by { it.a } + .by { it.b } + .by { it.c } + .by { it.d } + .and { it, other -> it.e contentEquals other.e } + .isEqual + } + + @Benchmark + fun apacheCommonsEqualsBuilderJava(): Boolean { + return EqualsBuilder() + .append(objA.a, objB.a) + .append(objA.b, objB.b) + .append(objA.c, objB.c) + .append(objA.d, objB.d) + .append(objA.e, objB.e) + .isEquals + } +} diff --git a/src/main/java/ru/nucodelabs/gem/file/dto/mapper/DtoMapper.java b/src/main/java/ru/nucodelabs/gem/file/dto/mapper/DtoMapper.java index eb79f101..b70828fb 100644 --- a/src/main/java/ru/nucodelabs/gem/file/dto/mapper/DtoMapper.java +++ b/src/main/java/ru/nucodelabs/gem/file/dto/mapper/DtoMapper.java @@ -4,21 +4,21 @@ import ru.nucodelabs.gem.file.dto.anisotropy.*; import ru.nucodelabs.geo.anisotropy.*; +import java.util.Collections; import java.util.List; -import static org.apache.commons.collections4.ListUtils.emptyIfNull; @Mapper( componentModel = MappingConstants.ComponentModel.JSR330, - unmappedSourcePolicy = ReportingPolicy.ERROR, - unmappedTargetPolicy = ReportingPolicy.ERROR, - nullValueIterableMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT + unmappedSourcePolicy = ReportingPolicy.ERROR, + unmappedTargetPolicy = ReportingPolicy.ERROR, + nullValueIterableMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT ) public abstract class DtoMapper { @Mapping( - target = "resistanceApparent", - defaultExpression = "java(ru.nucodelabs.geo.ves.calc.VesKt.rhoA(dto.getAb2(), dto.getMn2(), dto.getAmperage(), dto.getVoltage()))" + target = "resistanceApparent", + defaultExpression = "java(ru.nucodelabs.geo.ves.calc.VesKt.rhoA(dto.getAb2(), dto.getMn2(), dto.getAmperage(), dto.getVoltage()))" ) @Mapping(target = "errorResistanceApparent", defaultExpression = "java(ru.nucodelabs.geo.anisotropy.DefaultValues.DEFAULT_ERROR)") @Mapping(target = "isHidden", source = "hidden", defaultValue = "false") @@ -32,7 +32,7 @@ public abstract class DtoMapper { protected abstract FixableValue fromDto(FixableDoubleValueDto fixableDoubleValueDto); protected Signals mapSignals(List dto) { - return new Signals(emptyIfNull(dto).stream().map(this::fromDto).toList()); + return new Signals((dto != null ? dto : Collections.emptyList()).stream().map(this::fromDto).toList()); } @Mapping(target = "isFixed", source = "fixed") diff --git a/src/main/java/ru/nucodelabs/gem/view/color/ColorPalette.java b/src/main/java/ru/nucodelabs/gem/view/color/ColorPalette.java index 1d71ce77..bae0f35a 100644 --- a/src/main/java/ru/nucodelabs/gem/view/color/ColorPalette.java +++ b/src/main/java/ru/nucodelabs/gem/view/color/ColorPalette.java @@ -5,7 +5,7 @@ import org.jetbrains.annotations.NotNull; import ru.nucodelabs.files.clr.ColorNode; import ru.nucodelabs.files.clr.RgbaColor; -import ru.nucodelabs.util.std.MathKt; +import ru.nucodelabs.util.MathKt; import java.util.ArrayList; import java.util.Arrays; diff --git a/src/main/java/ru/nucodelabs/gem/view/control/chart/log/LogarithmicAxis.kt b/src/main/java/ru/nucodelabs/gem/view/control/chart/log/LogarithmicAxis.kt index 96ef78f1..8ee600ea 100644 --- a/src/main/java/ru/nucodelabs/gem/view/control/chart/log/LogarithmicAxis.kt +++ b/src/main/java/ru/nucodelabs/gem/view/control/chart/log/LogarithmicAxis.kt @@ -9,7 +9,7 @@ import javafx.beans.property.DoubleProperty import javafx.beans.property.SimpleDoubleProperty import javafx.util.Duration import ru.nucodelabs.gem.view.control.chart.InvertibleValueAxis -import ru.nucodelabs.util.std.exp10 +import ru.nucodelabs.util.exp10 import kotlin.math.ceil import kotlin.math.floor import kotlin.math.log10 @@ -88,8 +88,10 @@ open class LogarithmicAxis @JvmOverloads constructor( tickMarksTextNodes.forEach { (mark, node) -> node.isVisible = mark.isTextVisible } } - private fun checkBounds(lowerBound: Double, upperBound: Double) = - require(lowerBound > 0 && upperBound > 0 && lowerBound <= upperBound) { "Lower: $lowerBound; Upper: $upperBound" } + /* + private fun checkBounds(lowerBound: Double, upperBound: Double) = + require(lowerBound > 0 && upperBound > 0 && lowerBound <= upperBound) { "Lower: $lowerBound; Upper: $upperBound" } + */ override fun calculateMinorTickMarks(): List { @@ -185,7 +187,7 @@ open class LogarithmicAxis @JvmOverloads constructor( ) lowerRangeTimeline.play() upperRangeTimeline.play() - } catch (e: Exception) { + } catch (_: Exception) { lowerBoundProperty().set(lowerBound.toDouble()) upperBoundProperty().set(upperBound.toDouble()) } diff --git a/src/main/java/ru/nucodelabs/gem/view/control/chart/log/LogarithmicChartNavigationSupport.kt b/src/main/java/ru/nucodelabs/gem/view/control/chart/log/LogarithmicChartNavigationSupport.kt index d90c834d..957145fe 100644 --- a/src/main/java/ru/nucodelabs/gem/view/control/chart/log/LogarithmicChartNavigationSupport.kt +++ b/src/main/java/ru/nucodelabs/gem/view/control/chart/log/LogarithmicChartNavigationSupport.kt @@ -2,7 +2,7 @@ package ru.nucodelabs.gem.view.control.chart.log import javafx.scene.chart.ValueAxis import ru.nucodelabs.kfx.ext.length -import ru.nucodelabs.util.std.exp10 +import ru.nucodelabs.util.exp10 import kotlin.math.log10 import kotlin.math.max import kotlin.math.min diff --git a/src/main/java/ru/nucodelabs/gem/view/control/chart/log/PseudoLogarithmicAxis.kt b/src/main/java/ru/nucodelabs/gem/view/control/chart/log/PseudoLogarithmicAxis.kt index 998d5f0b..b99b826a 100644 --- a/src/main/java/ru/nucodelabs/gem/view/control/chart/log/PseudoLogarithmicAxis.kt +++ b/src/main/java/ru/nucodelabs/gem/view/control/chart/log/PseudoLogarithmicAxis.kt @@ -1,7 +1,7 @@ package ru.nucodelabs.gem.view.control.chart.log import javafx.beans.NamedArg -import ru.nucodelabs.util.std.exp10 +import ru.nucodelabs.util.exp10 import kotlin.math.* class PseudoLogarithmicAxis @JvmOverloads constructor( @@ -16,8 +16,10 @@ class PseudoLogarithmicAxis @JvmOverloads constructor( // checkBounds(lowerBound, upperBound) } - private fun checkBounds(lowerBound: Double, upperBound: Double) = - require(upperBound >= lowerBound) + /* + private fun checkBounds(lowerBound: Double, upperBound: Double) = + require(upperBound >= lowerBound) + */ @Suppress("UNCHECKED_CAST") override fun calculateTickValues(length: Double, range: Any?): List { diff --git a/src/main/java/ru/nucodelabs/gem/view/controller/anisotropy/main/AnisotropyMainViewController.kt b/src/main/java/ru/nucodelabs/gem/view/controller/anisotropy/main/AnisotropyMainViewController.kt index 90003207..2c693a5f 100644 --- a/src/main/java/ru/nucodelabs/gem/view/controller/anisotropy/main/AnisotropyMainViewController.kt +++ b/src/main/java/ru/nucodelabs/gem/view/controller/anisotropy/main/AnisotropyMainViewController.kt @@ -37,7 +37,7 @@ import ru.nucodelabs.gem.view.mapping.mapSignals import ru.nucodelabs.gem.view.mapping.mapSignalsRelations import ru.nucodelabs.kfx.core.AbstractViewController import ru.nucodelabs.kfx.ext.* -import ru.nucodelabs.util.std.toDoubleOrNullBy +import ru.nucodelabs.util.toDoubleOrNullBy import java.io.File import java.net.URL import java.text.DecimalFormat diff --git a/src/main/java/ru/nucodelabs/gem/view/controller/charts/ColorAxisController.kt b/src/main/java/ru/nucodelabs/gem/view/controller/charts/ColorAxisController.kt index 232136e2..15781f21 100644 --- a/src/main/java/ru/nucodelabs/gem/view/controller/charts/ColorAxisController.kt +++ b/src/main/java/ru/nucodelabs/gem/view/controller/charts/ColorAxisController.kt @@ -26,7 +26,7 @@ import ru.nucodelabs.gem.view.control.chart.log.LogarithmicAxis import ru.nucodelabs.kfx.core.AbstractViewController import ru.nucodelabs.kfx.ext.* import ru.nucodelabs.kfx.pref.FXPreferences -import ru.nucodelabs.util.std.toDoubleOrNullBy +import ru.nucodelabs.util.toDoubleOrNullBy import tornadofx.div import tornadofx.minus import java.net.URL diff --git a/src/main/java/ru/nucodelabs/gem/view/controller/charts/VesCurvesController.kt b/src/main/java/ru/nucodelabs/gem/view/controller/charts/VesCurvesController.kt index a0832cd8..0739690d 100644 --- a/src/main/java/ru/nucodelabs/gem/view/controller/charts/VesCurvesController.kt +++ b/src/main/java/ru/nucodelabs/gem/view/controller/charts/VesCurvesController.kt @@ -37,7 +37,7 @@ import ru.nucodelabs.geo.ves.calc.resistanceApparentUpperBoundByError import ru.nucodelabs.geo.ves.calc.zOfModelLayers import ru.nucodelabs.kfx.ext.* import ru.nucodelabs.kfx.snapshot.HistoryManager -import ru.nucodelabs.util.std.exp10 +import ru.nucodelabs.util.exp10 import java.lang.Double.max import java.lang.Double.min import java.net.URL diff --git a/src/main/java/ru/nucodelabs/gem/view/controller/main/InitialModelConfigurationViewController.kt b/src/main/java/ru/nucodelabs/gem/view/controller/main/InitialModelConfigurationViewController.kt index 31ddd3e6..32fba2cd 100644 --- a/src/main/java/ru/nucodelabs/gem/view/controller/main/InitialModelConfigurationViewController.kt +++ b/src/main/java/ru/nucodelabs/gem/view/controller/main/InitialModelConfigurationViewController.kt @@ -8,8 +8,8 @@ import ru.nucodelabs.gem.fxmodel.ves.app.ArbitraryInitialModelParameters import ru.nucodelabs.gem.fxmodel.ves.app.VesFxAppModel import ru.nucodelabs.gem.view.AlertsFactory import ru.nucodelabs.kfx.core.AbstractViewController -import ru.nucodelabs.util.std.toDoubleOrNullBy -import ru.nucodelabs.util.std.toIntOrNullBy +import ru.nucodelabs.util.toDoubleOrNullBy +import ru.nucodelabs.util.toIntOrNullBy import java.net.URL import java.text.DecimalFormat import java.util.* diff --git a/src/main/java/ru/nucodelabs/gem/view/controller/main/PicketsBarController.kt b/src/main/java/ru/nucodelabs/gem/view/controller/main/PicketsBarController.kt index e862316c..59bb339b 100644 --- a/src/main/java/ru/nucodelabs/gem/view/controller/main/PicketsBarController.kt +++ b/src/main/java/ru/nucodelabs/gem/view/controller/main/PicketsBarController.kt @@ -16,7 +16,7 @@ import ru.nucodelabs.geo.ves.Section import ru.nucodelabs.geo.ves.calc.length import ru.nucodelabs.geo.ves.calc.picketsBounds import ru.nucodelabs.kfx.snapshot.HistoryManager -import ru.nucodelabs.util.std.swap +import ru.nucodelabs.util.swap import java.net.URL import java.util.* diff --git a/src/main/java/ru/nucodelabs/gem/view/controller/tables/ExperimentalTableController.kt b/src/main/java/ru/nucodelabs/gem/view/controller/tables/ExperimentalTableController.kt index 094d7edd..30738442 100644 --- a/src/main/java/ru/nucodelabs/gem/view/controller/tables/ExperimentalTableController.kt +++ b/src/main/java/ru/nucodelabs/gem/view/controller/tables/ExperimentalTableController.kt @@ -42,7 +42,7 @@ import ru.nucodelabs.kfx.ext.decimalFilter import ru.nucodelabs.kfx.ext.toObservableList import ru.nucodelabs.kfx.snapshot.HistoryManager import ru.nucodelabs.util.TextToTableParser -import ru.nucodelabs.util.std.toDoubleOrNullBy +import ru.nucodelabs.util.toDoubleOrNullBy import tornadofx.getValue import java.net.URL import java.text.DecimalFormat @@ -261,7 +261,9 @@ class ExperimentalTableController @Inject constructor( style = "-fx-font-size: $DEFAULT_FONT_SIZE;" } - onContextMenuRequested = EventHandler { contextMenu.show(this, it.screenX, it.screenY) } + onContextMenuRequested = EventHandler { + if (item != null) contextMenu.show(this, it.screenX, it.screenY) + } } } } diff --git a/src/main/java/ru/nucodelabs/gem/view/controller/tables/ModelTableController.kt b/src/main/java/ru/nucodelabs/gem/view/controller/tables/ModelTableController.kt index 55d071a0..3b38536d 100644 --- a/src/main/java/ru/nucodelabs/gem/view/controller/tables/ModelTableController.kt +++ b/src/main/java/ru/nucodelabs/gem/view/controller/tables/ModelTableController.kt @@ -37,8 +37,10 @@ import ru.nucodelabs.geo.ves.calc.zOfModelLayers import ru.nucodelabs.geo.ves.toTabulatedTable import ru.nucodelabs.kfx.ext.toObservableList import ru.nucodelabs.kfx.snapshot.HistoryManager +import ru.nucodelabs.util.Err +import ru.nucodelabs.util.Ok import ru.nucodelabs.util.TextToTableParser -import ru.nucodelabs.util.std.toDoubleOrNullBy +import ru.nucodelabs.util.toDoubleOrNullBy import tornadofx.getValue import java.net.URL import java.text.DecimalFormat @@ -238,7 +240,9 @@ class ModelTableController @Inject constructor( } } - onContextMenuRequested = EventHandler { createContextMenu().show(this, it.screenX, it.screenY) } + onContextMenuRequested = EventHandler { + if (item != null) createContextMenu().show(this, it.screenX, it.screenY) + } } } } @@ -278,6 +282,12 @@ class ModelTableController @Inject constructor( } } } + if (modelData.size > Picket.MAX_MODEL_DATA_SIZE) { + alertsFactory.simpleAlert( + text = "Количество слоев модели не должно превышать ${Picket.MAX_MODEL_DATA_SIZE}" + ).show() + return + } table.items.setAll(modelData.map { mapper.toObservable(it) }) } @@ -410,8 +420,17 @@ class ModelTableController @Inject constructor( private fun commitChanges() { val modelDataInTable = table.items.map { mapper.toModel(it) } if (modelDataInTable != picket.modelData) { - historyManager.snapshotAfter { - observableSection.pickets[picketIndex] = picket.copy(modelData = modelDataInTable) + val update = picket.copied(modelData = modelDataInTable) + when (update) { + is Err -> { + alertsFactory.simpleAlert(text = update.error.joinToString(separator = "\n")).show() + } + + is Ok -> { + historyManager.snapshotAfter { + observableSection.pickets[picketIndex] = update.value + } + } } } } diff --git a/src/main/java/ru/nucodelabs/geo/target/impl/SquareDiffErrorAwareTargetFunction.kt b/src/main/java/ru/nucodelabs/geo/target/impl/SquareDiffErrorAwareTargetFunction.kt index 1f4f5186..be1d2029 100644 --- a/src/main/java/ru/nucodelabs/geo/target/impl/SquareDiffErrorAwareTargetFunction.kt +++ b/src/main/java/ru/nucodelabs/geo/target/impl/SquareDiffErrorAwareTargetFunction.kt @@ -1,7 +1,7 @@ package ru.nucodelabs.geo.target.impl import ru.nucodelabs.geo.target.RelativeErrorAwareTargetFunction -import ru.nucodelabs.util.std.fromPercent +import ru.nucodelabs.util.fromPercent import kotlin.math.pow /** diff --git a/src/main/java/ru/nucodelabs/geo/ves/Picket.kt b/src/main/java/ru/nucodelabs/geo/ves/Picket.kt index 12805ee6..26213772 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/Picket.kt +++ b/src/main/java/ru/nucodelabs/geo/ves/Picket.kt @@ -3,9 +3,10 @@ package ru.nucodelabs.geo.ves import com.fasterxml.jackson.annotation.JsonGetter import com.fasterxml.jackson.annotation.JsonIgnore import jakarta.validation.Valid -import jakarta.validation.constraints.Min +import jakarta.validation.constraints.DecimalMin import jakarta.validation.constraints.Size -import ru.nucodelabs.geo.ves.calc.orderByDistances +import ru.nucodelabs.geo.ves.calc.ExperimentalDataSort +import ru.nucodelabs.util.* import java.util.* /** @@ -15,27 +16,26 @@ import java.util.* * @property modelData Данные модели * @property offsetX Расстояние до пикета слева * @property z Координата Z + * @property comment Комментарий */ -data class Picket( - @JsonIgnore val id: UUID = UUID.randomUUID(), - val name: String = DEFAULT_NAME, - private var experimentalData: List<@Valid ExperimentalData> = emptyList(), - @field:Size(max = 40) var modelData: List<@Valid ModelLayer> = emptyList(), - @field:Min(0) val offsetX: Double = DEFAULT_OFFSET_X, - val z: Double = DEFAULT_Z, - val comment: String = DEFAULT_COMMENT +class Picket private constructor( + val name: String, + experimentalData: List<@Valid ExperimentalData>, + modelData: List<@Valid ModelLayer>, + @get:DecimalMin(MIN_OFFSET_X.toString()) val offsetX: Double, + val z: Double, + val comment: String, + skipExperimentalDataProcessing: Boolean = false, + skipModelDataProcessing: Boolean = false, ) { - init { - modelData = modelData.toMutableList().also { - if (it.isNotEmpty()) { - for (i in it.indices) { - if (it[i].power.isNaN()) { - it[i] = it[i].copy(power = 0.0) - } - } - it[it.lastIndex] = it.last().copy(power = Double.NaN) - } - } + + @JsonIgnore + val id: UUID = UUID.randomUUID() + + @get:Size(max = MAX_MODEL_DATA_SIZE) + val modelData by lazy { + if (skipModelDataProcessing) return@lazy modelData + preprocessModelData(modelData) } /** @@ -43,7 +43,80 @@ data class Picket( */ @get:JsonGetter("experimentalData") val sortedExperimentalData: List by lazy { + if (skipExperimentalDataProcessing) return@lazy experimentalData + preprocessExperimentalData(experimentalData) + } + /** + * Без отключенных и если одинаковые AB/2, то с наибольшим MN/2 + */ + @get:JsonIgnore + val effectiveExperimentalData: List by lazy { + this.sortedExperimentalData.filter { !it.isHidden } + } + + override fun equals(other: Any?): Boolean { + return Equals(this, other) + .by { it.id } + .by { it.name } + .by { it.sortedExperimentalData } + .by { it.modelData } + .by { it.offsetX } + .by { it.z } + .by { it.comment } + .isEqual + } + + override fun hashCode(): Int { + return hash( + id, + name, + sortedExperimentalData, + modelData, + offsetX, + z, + comment + ) + } + + /** + * Slightly optimized copy + */ + fun copy( + name: String = this.name, + experimentalData: List = this.sortedExperimentalData, + modelData: List = this.modelData, + offsetX: Double = this.offsetX, + z: Double = this.z, + comment: String = this.comment + ): Picket = copied( + name, + experimentalData, + modelData, + offsetX, + z, + comment + ).okOrThrow { IllegalArgumentException(it.joinToString()) } + + fun copied( + name: String = this.name, + experimentalData: List = this.sortedExperimentalData, + modelData: List = this.modelData, + offsetX: Double = this.offsetX, + z: Double = this.z, + comment: String = this.comment + ): Result> = internalNew( + name, + experimentalData, + modelData, + offsetX, + z, + comment, + skipExperimentalDataProcessing = this.sortedExperimentalData === experimentalData, + skipModelDataProcessing = this.modelData === modelData + ) + + private fun preprocessExperimentalData(experimentalData: List): List { val acc = mutableListOf() // Группируем по AB @@ -51,7 +124,7 @@ data class Picket( for ((_, group) in dupGroups) { // Если больше одного не отключенного дубликата в группе acc += if (group.filter { !it.isHidden }.size > 1) { - val sortedGroup = group.sortedWith(orderByDistances()) + val sortedGroup = group.sortedWith(ExperimentalDataSort.orderByDistances) List(sortedGroup.size) { // Отключаем все кроме последнего (с наиб. MN) if (it < sortedGroup.lastIndex) { @@ -65,25 +138,99 @@ data class Picket( } } - acc.sortedWith(orderByDistances()) - } - - init { - experimentalData = sortedExperimentalData + return acc.sortedWith(ExperimentalDataSort.orderByDistances) } - /** - * Без отключенных и если одинаковые AB/2, то с наибольшим MN/2 - */ - @get:JsonIgnore - val effectiveExperimentalData: List by lazy { - sortedExperimentalData.filter { !it.isHidden } + fun preprocessModelData(raw: List): List { + return raw.toMutableList().also { + if (it.isNotEmpty()) { + for (i in it.indices) { + if (it[i].power.isNaN()) { + it[i] = it[i].copy(power = 0.0) + } + } + it[it.lastIndex] = it.last().copy(power = Double.NaN) + } + } } - companion object Defaults { + companion object { const val DEFAULT_OFFSET_X = 100.0 const val DEFAULT_NAME = "Пикет" const val DEFAULT_Z = 0.0 const val DEFAULT_COMMENT = "" + + const val MAX_MODEL_DATA_SIZE = 40 + const val MIN_OFFSET_X = 0.0 + + /** + * Validate and create + */ + fun new( + name: String = DEFAULT_NAME, + experimentalData: List = emptyList(), + modelData: List = emptyList(), + offsetX: Double = DEFAULT_OFFSET_X, + z: Double = DEFAULT_Z, + comment: String = DEFAULT_COMMENT + ): Result> { + return internalNew( + name, + experimentalData, + modelData, + offsetX, + z, + comment + ) + } + + private fun internalNew( + name: String, + experimentalData: List, + modelData: List, + offsetX: Double, + z: Double, + comment: String, + skipExperimentalDataProcessing: Boolean = false, + skipModelDataProcessing: Boolean = false + ): Result> { + val errors = ArrayList() + validate(modelData.size <= MAX_MODEL_DATA_SIZE) { + "Model layers count must be ≤ $MAX_MODEL_DATA_SIZE layers" + }?.let { errors += it } + validate(offsetX >= MIN_OFFSET_X) { "X-Offset must be zero or positive" }?.let { errors += it } + if (errors.isNotEmpty()) return Err(errors) + return Picket( + name, + experimentalData, + modelData, + offsetX, + z, + comment, + skipExperimentalDataProcessing, + skipModelDataProcessing + ).toOkResult() + } + + /** + * Classic style initialization. Throws on invalid input. + * + * Backwards compatibility. + */ + operator fun invoke( + name: String = DEFAULT_NAME, + experimentalData: List = emptyList(), + modelData: List = emptyList(), + offsetX: Double = DEFAULT_OFFSET_X, + z: Double = DEFAULT_Z, + comment: String = DEFAULT_COMMENT + ): Picket = new( + name, + experimentalData, + modelData, + offsetX, + z, + comment + ).okOrThrow { IllegalArgumentException(it.joinToString()) } } -} +} \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/geo/ves/calc/ExperimentalDataFunctions.kt b/src/main/java/ru/nucodelabs/geo/ves/calc/ExperimentalDataFunctions.kt index 44213297..2cf32645 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/calc/ExperimentalDataFunctions.kt +++ b/src/main/java/ru/nucodelabs/geo/ves/calc/ExperimentalDataFunctions.kt @@ -2,9 +2,9 @@ package ru.nucodelabs.geo.ves.calc import ru.nucodelabs.geo.ves.ExperimentalData -fun orderByDistances() = - compareBy { it.ab2 } - .thenBy { it.mn2 } +object ExperimentalDataSort { + val orderByDistances = compareBy { it.ab2 }.thenBy { it.mn2 } +} fun ExperimentalData.withCalculatedResistanceApparent() = this.copy(resistanceApparent = rhoA(ab2, mn2, amperage, voltage)) diff --git a/src/main/java/ru/nucodelabs/geo/ves/calc/Normalization.kt b/src/main/java/ru/nucodelabs/geo/ves/calc/Normalization.kt index 31a082bb..401872c6 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/calc/Normalization.kt +++ b/src/main/java/ru/nucodelabs/geo/ves/calc/Normalization.kt @@ -2,7 +2,7 @@ package ru.nucodelabs.geo.ves.calc import ru.nucodelabs.geo.ves.ExperimentalData import ru.nucodelabs.mathves.Normalization -import ru.nucodelabs.util.std.fromPercent +import ru.nucodelabs.util.fromPercent data class FixableValue(val value: T, val isFixed: Boolean) diff --git a/src/main/java/ru/nucodelabs/geo/ves/calc/error/SchlumbergerErrorFunctions.kt b/src/main/java/ru/nucodelabs/geo/ves/calc/error/SchlumbergerErrorFunctions.kt index 408198f5..c53d8fe2 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/calc/error/SchlumbergerErrorFunctions.kt +++ b/src/main/java/ru/nucodelabs/geo/ves/calc/error/SchlumbergerErrorFunctions.kt @@ -1,8 +1,8 @@ package ru.nucodelabs.geo.ves.calc.error import ru.nucodelabs.mathves.SchlumbergerErrorFunctions -import ru.nucodelabs.util.std.asPercent -import ru.nucodelabs.util.std.fromPercent +import ru.nucodelabs.util.asPercent +import ru.nucodelabs.util.fromPercent data class MinMax(val min: Double, val max: Double) diff --git a/src/main/java/ru/nucodelabs/geo/ves/calc/interpolation/Interpolator.kt b/src/main/java/ru/nucodelabs/geo/ves/calc/interpolation/Interpolator.kt index a8f0dbf2..f4ebda6c 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/calc/interpolation/Interpolator.kt +++ b/src/main/java/ru/nucodelabs/geo/ves/calc/interpolation/Interpolator.kt @@ -3,7 +3,7 @@ package ru.nucodelabs.geo.ves.calc.interpolation import org.apache.commons.math3.analysis.interpolation.BicubicInterpolatingFunction import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction import ru.nucodelabs.util.Point -import ru.nucodelabs.util.std.exp10 +import ru.nucodelabs.util.exp10 class Interpolator( private var interpolatorContext: InterpolatorContext diff --git a/src/main/java/ru/nucodelabs/geo/ves/calc/interpolation/SmartInterpolator.kt b/src/main/java/ru/nucodelabs/geo/ves/calc/interpolation/SmartInterpolator.kt index 4faffbea..c1bd0e7c 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/calc/interpolation/SmartInterpolator.kt +++ b/src/main/java/ru/nucodelabs/geo/ves/calc/interpolation/SmartInterpolator.kt @@ -2,7 +2,7 @@ package ru.nucodelabs.geo.ves.calc.interpolation import org.apache.commons.math3.analysis.interpolation.BicubicInterpolatingFunction import ru.nucodelabs.util.Point -import ru.nucodelabs.util.std.exp10 +import ru.nucodelabs.util.exp10 import kotlin.math.log10 class SmartInterpolator( diff --git a/src/test/java/ru/nucodelabs/geo/ves/ExperimentalDataTest.kt b/src/test/java/ru/nucodelabs/geo/ves/ExperimentalDataTest.kt index b1581c6d..64d86917 100644 --- a/src/test/java/ru/nucodelabs/geo/ves/ExperimentalDataTest.kt +++ b/src/test/java/ru/nucodelabs/geo/ves/ExperimentalDataTest.kt @@ -1,7 +1,7 @@ package ru.nucodelabs.geo.ves import org.junit.jupiter.api.Test -import ru.nucodelabs.geo.ves.calc.orderByDistances +import ru.nucodelabs.geo.ves.calc.ExperimentalDataSort internal class ExperimentalDataTest { @@ -14,7 +14,7 @@ internal class ExperimentalDataTest { data.copy(mn2 = 0.5), data.copy(isHidden = true) ) - list.sortedWith(orderByDistances()).forEach { println(it) } + list.sortedWith(ExperimentalDataSort.orderByDistances).forEach { println(it) } val picket = Picket(experimentalData = list) println("initial") From 3f64e98b7391a6c59f0f8d54c1583da01b0b630b Mon Sep 17 00:00:00 2001 From: Vadim Mostovoy Date: Mon, 8 Sep 2025 00:52:23 +0700 Subject: [PATCH 05/18] Add Result.kt helpers --- .../src/main/kotlin/ru/nucodelabs/util/Result.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/common-utils/src/main/kotlin/ru/nucodelabs/util/Result.kt b/common-utils/src/main/kotlin/ru/nucodelabs/util/Result.kt index 5508df3e..023e91d8 100644 --- a/common-utils/src/main/kotlin/ru/nucodelabs/util/Result.kt +++ b/common-utils/src/main/kotlin/ru/nucodelabs/util/Result.kt @@ -7,6 +7,7 @@ class Ok(val value: T) : Result class Err(val error: E) : Result fun T.toOkResult(): Result = Ok(this) +fun E.toErrorResult(): Result = Err(this) inline fun Result.okOrThrow( mapError: (E) -> Throwable = { IllegalStateException(it.toString()) } @@ -17,9 +18,18 @@ inline fun Result.okOrThrow( } } -inline fun Result.onError(onError: (E) -> Unit) { +inline fun Result.ifError(onError: (E) -> Unit): Result { when (this) { is Err -> onError(error) - else -> return + else -> return this } + return this +} + +inline fun Result.ifOk(onOk: (T) -> Unit): Result { + when (this) { + is Ok -> onOk(value) + else -> return this + } + return this } \ No newline at end of file From 96e3c708da29a8190f8dd449dc91b68578a6f033 Mon Sep 17 00:00:00 2001 From: Vadim Mostovoy Date: Mon, 8 Sep 2025 00:55:25 +0700 Subject: [PATCH 06/18] Fix Result.kt helpers --- .../src/main/kotlin/ru/nucodelabs/util/Result.kt | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/common-utils/src/main/kotlin/ru/nucodelabs/util/Result.kt b/common-utils/src/main/kotlin/ru/nucodelabs/util/Result.kt index 023e91d8..5f3a2698 100644 --- a/common-utils/src/main/kotlin/ru/nucodelabs/util/Result.kt +++ b/common-utils/src/main/kotlin/ru/nucodelabs/util/Result.kt @@ -18,18 +18,12 @@ inline fun Result.okOrThrow( } } -inline fun Result.ifError(onError: (E) -> Unit): Result { - when (this) { - is Err -> onError(error) - else -> return this - } +inline fun Result.ifError(action: (E) -> Unit): Result { + if (this is Err) action(error) return this } -inline fun Result.ifOk(onOk: (T) -> Unit): Result { - when (this) { - is Ok -> onOk(value) - else -> return this - } +inline fun Result.ifOk(action: (T) -> Unit): Result { + if (this is Ok) action(value) return this } \ No newline at end of file From 94f3970627a32fbc10e98c902e685b61cc13904e Mon Sep 17 00:00:00 2001 From: Vadim Mostovoy Date: Tue, 9 Sep 2025 18:48:30 +0700 Subject: [PATCH 07/18] Optimize Model Section chart rendering speed. Add CanvasRenderPolygonChart. Remove PolygonWithNamesChart. Fix issues. Refactor. --- .../main/kotlin/ru/nucodelabs/kfx/ext/FX.kt | 18 ++++ .../gem/view/control/chart/AbstractMap.kt | 45 ++++++++-- .../control/chart/CanvasRenderPolygonChart.kt | 69 +++++++++++++++ .../view/control/chart/InterpolationMap.kt | 36 +++----- .../gem/view/control/chart/PolygonChart.kt | 4 + .../control/chart/PolygonWithNamesChart.kt | 85 ------------------- .../control/chart/SmartInterpolationMap.kt | 23 ++--- .../charts/ModelSectionController.kt | 33 +++---- .../charts/ModelSectionSwitcherController.kt | 44 ++++------ .../controller/charts/LinearModelSection.fxml | 11 ++- .../charts/LogarithmicModelSection.fxml | 10 +-- .../charts/ModelSectionSwitcher.fxml | 2 +- .../gem/view/control/chart/AbstractMapTest.kt | 6 +- 13 files changed, 186 insertions(+), 200 deletions(-) create mode 100644 src/main/java/ru/nucodelabs/gem/view/control/chart/CanvasRenderPolygonChart.kt delete mode 100644 src/main/java/ru/nucodelabs/gem/view/control/chart/PolygonWithNamesChart.kt diff --git a/kfx-utils/src/main/kotlin/ru/nucodelabs/kfx/ext/FX.kt b/kfx-utils/src/main/kotlin/ru/nucodelabs/kfx/ext/FX.kt index 1a0e0185..1a30fa3b 100644 --- a/kfx-utils/src/main/kotlin/ru/nucodelabs/kfx/ext/FX.kt +++ b/kfx-utils/src/main/kotlin/ru/nucodelabs/kfx/ext/FX.kt @@ -12,6 +12,7 @@ import javafx.collections.FXCollections import javafx.collections.ObservableList import javafx.embed.swing.SwingFXUtils import javafx.scene.Node +import javafx.scene.Parent import javafx.scene.SnapshotParameters import javafx.scene.canvas.Canvas import javafx.scene.chart.XYChart @@ -271,4 +272,21 @@ fun unfocus(node: Node) { if (node.isFocused) { node.parent.requestFocus() } +} + +fun switch(from: Parent, to: Parent) { + hide(from) + show(to) + to.requestLayout() + to.layout() +} + +fun hide(node: Node) { + node.isVisible = false + node.isManaged = false +} + +fun show(node: Node) { + node.isManaged = true + node.isVisible = true } \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/gem/view/control/chart/AbstractMap.kt b/src/main/java/ru/nucodelabs/gem/view/control/chart/AbstractMap.kt index 07439d97..d9eed5a7 100644 --- a/src/main/java/ru/nucodelabs/gem/view/control/chart/AbstractMap.kt +++ b/src/main/java/ru/nucodelabs/gem/view/control/chart/AbstractMap.kt @@ -1,26 +1,53 @@ package ru.nucodelabs.gem.view.control.chart import javafx.beans.NamedArg +import javafx.beans.property.SimpleObjectProperty import javafx.scene.canvas.Canvas import javafx.scene.chart.ScatterChart import javafx.scene.chart.ValueAxis import javafx.scene.layout.Region +import ru.nucodelabs.gem.view.color.ColorMapper abstract class AbstractMap( @NamedArg("xAxis") xAxis: ValueAxis, - @NamedArg("yAxis") yAxis: ValueAxis + @NamedArg("yAxis") yAxis: ValueAxis, + @NamedArg("colorMapper") colorMapper: ColorMapper? = null ) : ScatterChart(xAxis, yAxis) { - private val plotArea = this.lookup(".chart-plot-background") as Region + private val _colorMapper = SimpleObjectProperty(colorMapper) + fun colorMapperProperty() = _colorMapper + var colorMapper: ColorMapper? + set(value) = _colorMapper.set(value) + get() = _colorMapper.get() - protected val canvas: Canvas = Canvas(plotArea.width, plotArea.height) + init { + colorMapperProperty().addListener { _, _, new -> + startListening(new) + onColorMapperChange() + } + startListening(colorMapper) + } + + private fun startListening(colorMapper: ColorMapper?) { + colorMapper?.minValueProperty()?.addListener { _, _, _ -> onColorMapperChange() } + colorMapper?.maxValueProperty()?.addListener { _, _, _ -> onColorMapperChange() } + colorMapper?.numberOfSegmentsProperty()?.addListener { _, _, _ -> onColorMapperChange() } + colorMapper?.logScaleProperty()?.addListener { _, _, _ -> onColorMapperChange() } + } + + protected val plotBackground = this.lookup(".chart-plot-background") as Region + + protected val backgroundCanvas: Canvas = Canvas(plotBackground.width, plotBackground.height) init { - plotChildren += canvas - canvas.layoutX = 0.0 - canvas.layoutY = 0.0 - canvas.widthProperty().bind(plotArea.widthProperty()) - canvas.heightProperty().bind(plotArea.heightProperty()) - canvas.viewOrder = 1.0 + plotChildren += backgroundCanvas + backgroundCanvas.layoutX = 0.0 + backgroundCanvas.layoutY = 0.0 + backgroundCanvas.widthProperty().bind(plotBackground.widthProperty()) + backgroundCanvas.heightProperty().bind(plotBackground.heightProperty()) + backgroundCanvas.managedProperty().bind(this.managedProperty()) + backgroundCanvas.viewOrder = 1.0 } + + protected abstract fun onColorMapperChange() } \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/gem/view/control/chart/CanvasRenderPolygonChart.kt b/src/main/java/ru/nucodelabs/gem/view/control/chart/CanvasRenderPolygonChart.kt new file mode 100644 index 00000000..8fb13d10 --- /dev/null +++ b/src/main/java/ru/nucodelabs/gem/view/control/chart/CanvasRenderPolygonChart.kt @@ -0,0 +1,69 @@ +package ru.nucodelabs.gem.view.control.chart + +import javafx.beans.NamedArg +import javafx.beans.property.BooleanProperty +import javafx.beans.property.ObjectProperty +import javafx.beans.property.SimpleBooleanProperty +import javafx.beans.property.SimpleObjectProperty +import javafx.scene.canvas.Canvas +import javafx.scene.chart.ValueAxis +import javafx.scene.paint.Color +import javafx.util.StringConverter +import ru.nucodelabs.gem.view.color.ColorMapper +import tornadofx.getValue +import tornadofx.setValue + +class CanvasRenderPolygonChart @JvmOverloads constructor( + @NamedArg("xAxis") xAxis: ValueAxis, + @NamedArg("yAxis") yAxis: ValueAxis, + @NamedArg("colorMapper") colorMapper: ColorMapper? = null, +) : AbstractMap(xAxis, yAxis, colorMapper) { + + init { + animated = false + } + + private val extraValueFormatterProperty = SimpleObjectProperty?>(null).apply { + addListener { _, old, new -> render(backgroundCanvas) } + } + var extraValueFormatter: StringConverter? by extraValueFormatterProperty + fun extraValueFormatterProperty(): ObjectProperty?> = extraValueFormatterProperty + + private val extraValueVisibleProperty = SimpleBooleanProperty(false).apply { + addListener { _, old, new -> render(backgroundCanvas) } + } + var extraValueVisible by extraValueVisibleProperty + fun extraValueVisibleProperty(): BooleanProperty = extraValueVisibleProperty + + override fun onColorMapperChange() = render(backgroundCanvas) + + override fun layoutPlotChildren() { + super.layoutPlotChildren() + render(backgroundCanvas) + } + + private fun render(canvas: Canvas) { + val graphics = canvas.graphicsContext2D + data.forEach { series -> + if (series.data.isEmpty()) return@forEach + val (xPoints, yPoints) = toPolygon(series) + val extraValue = series.data.first().extraValue as Number + val color = colorMapper?.colorFor(extraValue.toDouble()) ?: Color.WHITE + graphics.fill = color + graphics.fillPolygon(xPoints, yPoints, xPoints.size) + if (extraValueVisible) { + val text = extraValueFormatter?.toString(extraValue) ?: extraValue.toString() + val textX = xPoints.min() + val textY = yPoints.max() + graphics.fill = color.invert() + graphics.fillText(text, textX, textY) + } + } + } + + private fun toPolygon(series: Series): Pair { + val xPoints = series.data.map { xAxis.getDisplayPosition(it.xValue) }.toDoubleArray() + val yPoints = series.data.map { yAxis.getDisplayPosition(it.yValue) }.toDoubleArray() + return Pair(xPoints, yPoints) + } +} \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/gem/view/control/chart/InterpolationMap.kt b/src/main/java/ru/nucodelabs/gem/view/control/chart/InterpolationMap.kt index 6cfa16ee..48d05f8b 100644 --- a/src/main/java/ru/nucodelabs/gem/view/control/chart/InterpolationMap.kt +++ b/src/main/java/ru/nucodelabs/gem/view/control/chart/InterpolationMap.kt @@ -2,7 +2,6 @@ package ru.nucodelabs.gem.view.control.chart import javafx.beans.NamedArg import javafx.beans.property.SimpleIntegerProperty -import javafx.beans.property.SimpleObjectProperty import javafx.scene.canvas.Canvas import javafx.scene.chart.ValueAxis import javafx.scene.effect.BlendMode @@ -20,9 +19,9 @@ class InterpolationMap @JvmOverloads constructor( @NamedArg("xAxis") xAxis: ValueAxis, @NamedArg("yAxis") yAxis: ValueAxis, @NamedArg("colorMapper") colorMapper: ColorMapper? = null -) : AbstractMap(xAxis, yAxis) { +) : AbstractMap(xAxis, yAxis, colorMapper) { - var canvasBlendMode: BlendMode by canvas.blendModeProperty() + var canvasBlendMode: BlendMode by backgroundCanvas.blendModeProperty() private val _interpolateSeriesIndex = SimpleIntegerProperty(0) @@ -46,27 +45,6 @@ class InterpolationMap @JvmOverloads constructor( } } - private val _colorMapper = SimpleObjectProperty(colorMapper) - fun colorMapperProperty() = _colorMapper - var colorMapper: ColorMapper? - set(value) = _colorMapper.set(value) - get() = _colorMapper.get() - - init { - colorMapperProperty().addListener { _, _, new -> - startListening(new) - redrawSnapshot() - } - startListening(colorMapper) - } - - private fun startListening(colorMapper: ColorMapper?) { - colorMapper?.minValueProperty()?.addListener { _, _, _ -> redrawSnapshot() } - colorMapper?.maxValueProperty()?.addListener { _, _, _ -> redrawSnapshot() } - colorMapper?.numberOfSegmentsProperty()?.addListener { _, _, _ -> redrawSnapshot() } - colorMapper?.logScaleProperty()?.addListener { _, _, _ -> redrawSnapshot() } - } - private lateinit var interpolator: Interpolator private var preparedData: List> = mutableListOf() @@ -75,6 +53,8 @@ class InterpolationMap @JvmOverloads constructor( if (needRedraw) redrawSnapshot() else layoutSnapshot() } + override fun onColorMapperChange() = redrawSnapshot() + private fun redrawSnapshot() { initInterpolator() needRedraw = false @@ -85,7 +65,13 @@ class InterpolationMap @JvmOverloads constructor( } private fun layoutSnapshot() { - canvas.graphicsContext2D.drawImage(snapshot, 0.0, 0.0, canvas.width, canvas.height) + backgroundCanvas.graphicsContext2D.drawImage( + snapshot, + 0.0, + 0.0, + backgroundCanvas.width, + backgroundCanvas.height + ) } override fun dataItemAdded(series: Series?, itemIndex: Int, item: Data?) { diff --git a/src/main/java/ru/nucodelabs/gem/view/control/chart/PolygonChart.kt b/src/main/java/ru/nucodelabs/gem/view/control/chart/PolygonChart.kt index 857113aa..667786f2 100644 --- a/src/main/java/ru/nucodelabs/gem/view/control/chart/PolygonChart.kt +++ b/src/main/java/ru/nucodelabs/gem/view/control/chart/PolygonChart.kt @@ -9,6 +9,10 @@ import javafx.scene.shape.Polygon * Draws polygons using points of each series. * To get polygons references use `seriesPolygons` map view. */ +@Deprecated( + "Use CanvasRenderPolygonChart", + ReplaceWith("CanvasRenderPolygonChart(xAxis, yAxis)") +) open class PolygonChart( @NamedArg("xAxis") xAxis: ValueAxis, @NamedArg("yAxis") yAxis: ValueAxis diff --git a/src/main/java/ru/nucodelabs/gem/view/control/chart/PolygonWithNamesChart.kt b/src/main/java/ru/nucodelabs/gem/view/control/chart/PolygonWithNamesChart.kt deleted file mode 100644 index a701e8cd..00000000 --- a/src/main/java/ru/nucodelabs/gem/view/control/chart/PolygonWithNamesChart.kt +++ /dev/null @@ -1,85 +0,0 @@ -package ru.nucodelabs.gem.view.control.chart - -import javafx.beans.NamedArg -import javafx.beans.property.BooleanProperty -import javafx.beans.property.SimpleBooleanProperty -import javafx.scene.chart.ValueAxis -import javafx.scene.effect.DropShadow -import javafx.scene.paint.Color -import javafx.scene.text.Font -import javafx.scene.text.Text -import tornadofx.getValue -import tornadofx.setValue - -class PolygonWithNamesChart( - @NamedArg("xAxis") xAxis: ValueAxis, - @NamedArg("yAxis") yAxis: ValueAxis -) : PolygonChart(xAxis, yAxis) { - - private val namesVisibleProperty = SimpleBooleanProperty(false) - var namesVisible by namesVisibleProperty - fun namesVisibleProperty(): BooleanProperty = namesVisibleProperty - - /** - * Maps series to corresponding Text node - */ - val seriesText: Map, Text> - get() = _seriesText - - private val _seriesText = mutableMapOf, Text>() - - private fun removeTextGroup(series: Series) { - plotChildren -= _seriesText[series] - _seriesText -= series - } - - override fun layoutPlotChildren() { - super.layoutPlotChildren() - setupTextAll() - } - - override fun seriesRemoved(series: Series) { - super.seriesRemoved(series) - removeTextGroup(series) - } - - override fun seriesAdded(series: Series, seriesIndex: Int) { - super.seriesAdded(series, seriesIndex) - setupText(series) - } - - private fun setupTextAll() { - for (series in data) { - setupText(series) - } - } - - private fun setupText(series: Series) { - val polygon = seriesPolygons[series] ?: return - - val xCoord = polygon.points[0] + 3 - val yCoord = polygon.points[1] + 10 - - val text = _seriesText[series] - if (text != null) { - text.x = xCoord - text.y = yCoord - } else { - val newText = Text(xCoord, yCoord, series.name ?: "").apply { - font = Font(11.0) - fill = Color.WHITE - effect = DropShadow(2.0, Color.BLACK) - textProperty().bind(series.nameProperty()) - managedProperty().bind(visibleProperty()) - isVisible = namesVisible - visibleProperty().bind(namesVisibleProperty) - } - plotChildren += newText - _seriesText[series] = newText - } - } -} - - - - diff --git a/src/main/java/ru/nucodelabs/gem/view/control/chart/SmartInterpolationMap.kt b/src/main/java/ru/nucodelabs/gem/view/control/chart/SmartInterpolationMap.kt index dd5818f0..c09656a8 100644 --- a/src/main/java/ru/nucodelabs/gem/view/control/chart/SmartInterpolationMap.kt +++ b/src/main/java/ru/nucodelabs/gem/view/control/chart/SmartInterpolationMap.kt @@ -1,37 +1,24 @@ package ru.nucodelabs.gem.view.control.chart import javafx.beans.NamedArg -import javafx.beans.property.ObjectProperty -import javafx.beans.property.SimpleObjectProperty import javafx.scene.chart.ValueAxis import ru.nucodelabs.gem.view.color.ColorMapper import ru.nucodelabs.geo.ves.calc.interpolation.ApacheInterpolator2D import ru.nucodelabs.geo.ves.calc.interpolation.RBFSpatialInterpolator import ru.nucodelabs.geo.ves.calc.interpolation.SmartInterpolator +// TODO: Optimize like InterpolationMap class SmartInterpolationMap( @NamedArg("xAxis") xAxis: ValueAxis, @NamedArg("yAxis") yAxis: ValueAxis, @NamedArg("colorMapper") colorMapper: ColorMapper? = null -) : AbstractMap(xAxis, yAxis) { +) : AbstractMap(xAxis, yAxis, colorMapper) { - private val _colorMapper = SimpleObjectProperty(colorMapper) private var interpolatorIsInitialized = false - fun colorMapperProperty(): ObjectProperty = _colorMapper - var colorMapper: ColorMapper? - set(value) = _colorMapper.set(value) - get() = _colorMapper.get() - - init { - colorMapperProperty().addListener { _, _, new -> - startListening(new) { draw() } - draw() - } - startListening(colorMapper) { draw() } - } - private val interpolator2D = SmartInterpolator(RBFSpatialInterpolator(), ApacheInterpolator2D()) + override fun onColorMapperChange() = draw() + override fun layoutPlotChildren() { super.layoutPlotChildren() if (!interpolatorIsInitialized) { @@ -61,6 +48,6 @@ class SmartInterpolationMap( } private fun draw() { - draw(canvas, xAxis, yAxis, interpolator2D, colorMapper) + draw(backgroundCanvas, xAxis, yAxis, interpolator2D, colorMapper) } } \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/gem/view/controller/charts/ModelSectionController.kt b/src/main/java/ru/nucodelabs/gem/view/controller/charts/ModelSectionController.kt index 7c829208..901e5f11 100644 --- a/src/main/java/ru/nucodelabs/gem/view/controller/charts/ModelSectionController.kt +++ b/src/main/java/ru/nucodelabs/gem/view/controller/charts/ModelSectionController.kt @@ -10,9 +10,9 @@ import javafx.stage.Stage import javafx.util.StringConverter import ru.nucodelabs.gem.fxmodel.ves.ObservableSection import ru.nucodelabs.gem.view.color.ColorMapper +import ru.nucodelabs.gem.view.control.chart.CanvasRenderPolygonChart import ru.nucodelabs.gem.view.control.chart.InvertibleValueAxis import ru.nucodelabs.gem.view.control.chart.NucodeNumberAxis -import ru.nucodelabs.gem.view.control.chart.PolygonWithNamesChart import ru.nucodelabs.gem.view.controller.AbstractController import ru.nucodelabs.gem.view.controller.charts.ModelSectionController.PicketDependencies.Factory.dependenciesOf import ru.nucodelabs.geo.ves.ExperimentalData @@ -25,7 +25,6 @@ import ru.nucodelabs.kfx.ext.observableListOf import java.math.MathContext import java.math.RoundingMode import java.net.URL -import java.text.DecimalFormat import java.util.* import kotlin.math.abs @@ -34,8 +33,7 @@ private const val LAST_COEF = 0.5 class ModelSectionController @Inject constructor( private val observableSection: ObservableSection, private val colorMapper: ColorMapper, - private val formatter: StringConverter, - private val df: DecimalFormat + private val formatter: StringConverter ) : AbstractController() { /** @@ -67,12 +65,14 @@ class ModelSectionController @Inject constructor( private lateinit var xAxis: NucodeNumberAxis @FXML - private lateinit var chart: PolygonWithNamesChart + lateinit var chart: CanvasRenderPolygonChart override val stage: Stage? get() = chart.scene.window as Stage? override fun initialize(location: URL, resources: ResourceBundle) { + chart.colorMapper = this.colorMapper + chart.extraValueFormatter = formatter yAxis.tickLabelFormatter = formatter xAxis.tickLabelFormatter = formatter @@ -89,11 +89,6 @@ class ModelSectionController @Inject constructor( } } }) - - colorMapper.maxValueProperty().addListener { _, _, _ -> update() } - colorMapper.minValueProperty().addListener { _, _, _ -> update() } - colorMapper.numberOfSegmentsProperty().addListener { _, _, _ -> update() } - colorMapper.logScaleProperty().addListener { _, _, _ -> update() } } private fun update() { @@ -109,6 +104,7 @@ class ModelSectionController @Inject constructor( val lowerBoundZ = zWithVirtualLastLayers().minOfOrNull { it.minOrNull() ?: 0.0 } ?: 0.0 + val dataToAdd = ArrayList>() for ((index, bounds) in observableSection.asSection().picketsBounds().withIndex()) { val picket = observableSection.pickets[index] if (picket.modelData.isEmpty()) { @@ -131,24 +127,21 @@ class ModelSectionController @Inject constructor( picket.modelData[i].power } + val resistance = picket.modelData[i].resistance val series: Series = Series( observableListOf( - Data(x, y), - Data(x + width, y), - Data(x + width, y - height), - Data(x, y - height) + Data(x, y, resistance), + Data(x + width, y, resistance), + Data(x + width, y - height, resistance), + Data(x, y - height, resistance) ) ) - chart.data += series - series.name = df.format(picket.modelData[i].resistance) - chart.seriesPolygons[series]?.apply { fill = colorMapper.colorFor(picket.modelData[i].resistance) } + dataToAdd += series } } - } - fun setupNames(boolean: Boolean) { - chart.namesVisibleProperty().set(boolean) + chart.data += dataToAdd } private fun setupXAxisMarks() { diff --git a/src/main/java/ru/nucodelabs/gem/view/controller/charts/ModelSectionSwitcherController.kt b/src/main/java/ru/nucodelabs/gem/view/controller/charts/ModelSectionSwitcherController.kt index ddaab82b..3d8507f0 100644 --- a/src/main/java/ru/nucodelabs/gem/view/controller/charts/ModelSectionSwitcherController.kt +++ b/src/main/java/ru/nucodelabs/gem/view/controller/charts/ModelSectionSwitcherController.kt @@ -9,12 +9,12 @@ import javafx.scene.control.ContextMenu import javafx.scene.control.MenuItem import javafx.scene.layout.VBox import javafx.stage.FileChooser -import javafx.stage.Stage import ru.nucodelabs.gem.app.pref.PNG_FILES_DIR import ru.nucodelabs.gem.config.Name -import ru.nucodelabs.gem.view.controller.AbstractController -import ru.nucodelabs.kfx.ext.bindTo +import ru.nucodelabs.kfx.core.AbstractViewController +import ru.nucodelabs.kfx.ext.hide import ru.nucodelabs.kfx.ext.saveSnapshotAsPng +import ru.nucodelabs.kfx.ext.switch import java.net.URL import java.util.* import java.util.prefs.Preferences @@ -22,7 +22,7 @@ import java.util.prefs.Preferences class ModelSectionSwitcherController @Inject constructor( @Named(Name.File.PNG) private val fc: FileChooser, private val prefs: Preferences -) : AbstractController() { +) : AbstractViewController() { @FXML private lateinit var logSectionBox: VBox @@ -35,30 +35,20 @@ class ModelSectionSwitcherController @Inject constructor( @FXML lateinit var linearSectionBoxController: ModelSectionController - override val stage: Stage? - get() = linearSectionBox.scene?.window as Stage? - override fun initialize(location: URL, resources: ResourceBundle) { - logSectionBox.managedProperty() bindTo logSectionBox.visibleProperty() - linearSectionBox.managedProperty() bindTo linearSectionBox.visibleProperty() - - logSectionBox.isVisible = false - logSectionBox.visibleProperty() bindTo linearSectionBox.visibleProperty().not() - linearSectionBox.isVisible = true + logSectionBoxController.chart.extraValueVisibleProperty() + .bindBidirectional(linearSectionBoxController.chart.extraValueVisibleProperty()) + hide(logSectionBox) logSectionBox.apply { val contextMenu = ContextMenu( MenuItem("Переключить на линейный масштаб").apply { - onAction = EventHandler { linearSectionBox.isVisible = true } + onAction = EventHandler { switch(logSectionBox, linearSectionBox) } }, - CheckMenuItem("Показывать сопротивления").apply{ + CheckMenuItem("Показывать сопротивления").apply { isSelected = false - onAction = if (!this.isSelected){ - EventHandler { logSectionBoxController.setupNames(this.isSelected)} - - }else { - EventHandler { logSectionBoxController.setupNames(this.isSelected) } - } + selectedProperty() + .bindBidirectional(logSectionBoxController.chart.extraValueVisibleProperty()) }, MenuItem("Сохранить как изображение").apply { onAction = EventHandler { @@ -77,16 +67,12 @@ class ModelSectionSwitcherController @Inject constructor( linearSectionBox.apply { val contextMenu = ContextMenu( MenuItem("Переключить на псевдо-логарифмический масштаб").apply { - onAction = EventHandler { linearSectionBox.isVisible = false } + onAction = EventHandler { switch(linearSectionBox, logSectionBox) } }, - CheckMenuItem("Показывать сопротивления").apply{ + CheckMenuItem("Показывать сопротивления").apply { isSelected = false - onAction = if (!this.isSelected){ - EventHandler { linearSectionBoxController.setupNames(this.isSelected)} - - }else { - EventHandler { linearSectionBoxController.setupNames(this.isSelected) } - } + selectedProperty() + .bindBidirectional(linearSectionBoxController.chart.extraValueVisibleProperty()) }, MenuItem("Сохранить как изображение").apply { onAction = EventHandler { diff --git a/src/main/resources/ru/nucodelabs/gem/view/controller/charts/LinearModelSection.fxml b/src/main/resources/ru/nucodelabs/gem/view/controller/charts/LinearModelSection.fxml index 89adbce5..88fe08f2 100644 --- a/src/main/resources/ru/nucodelabs/gem/view/controller/charts/LinearModelSection.fxml +++ b/src/main/resources/ru/nucodelabs/gem/view/controller/charts/LinearModelSection.fxml @@ -3,10 +3,9 @@ + - - - + @@ -34,5 +33,5 @@ - + \ No newline at end of file diff --git a/src/main/resources/ru/nucodelabs/gem/view/controller/charts/LogarithmicModelSection.fxml b/src/main/resources/ru/nucodelabs/gem/view/controller/charts/LogarithmicModelSection.fxml index 2aa93bf5..a2f0dc4c 100644 --- a/src/main/resources/ru/nucodelabs/gem/view/controller/charts/LogarithmicModelSection.fxml +++ b/src/main/resources/ru/nucodelabs/gem/view/controller/charts/LogarithmicModelSection.fxml @@ -3,9 +3,9 @@ + - \ No newline at end of file diff --git a/src/main/resources/ru/nucodelabs/gem/view/controller/charts/ModelSectionSwitcher.fxml b/src/main/resources/ru/nucodelabs/gem/view/controller/charts/ModelSectionSwitcher.fxml index 7dbacc16..215697ef 100644 --- a/src/main/resources/ru/nucodelabs/gem/view/controller/charts/ModelSectionSwitcher.fxml +++ b/src/main/resources/ru/nucodelabs/gem/view/controller/charts/ModelSectionSwitcher.fxml @@ -4,7 +4,7 @@ + maxWidth="Infinity" maxHeight="Infinity" fx:id="root"> diff --git a/src/test/java/ru/nucodelabs/gem/view/control/chart/AbstractMapTest.kt b/src/test/java/ru/nucodelabs/gem/view/control/chart/AbstractMapTest.kt index 5d1110ab..6c9a5aa0 100644 --- a/src/test/java/ru/nucodelabs/gem/view/control/chart/AbstractMapTest.kt +++ b/src/test/java/ru/nucodelabs/gem/view/control/chart/AbstractMapTest.kt @@ -22,14 +22,16 @@ internal class AbstractMapTest : FXTest() { ) { override fun layoutPlotChildren() { super.layoutPlotChildren() - canvas.clear() - draw(canvas) + backgroundCanvas.clear() + draw(backgroundCanvas) } override fun layoutChildren() { super.layoutChildren() } + override fun onColorMapperChange() {} + fun draw(canvas: Canvas) { canvas.run { graphicsContext2D.fill = Color.RED From 060f56534e7fca09b6a20050649248fc3c5f89d4 Mon Sep 17 00:00:00 2001 From: Vadim Mostovoy Date: Thu, 11 Sep 2025 12:24:06 +0700 Subject: [PATCH 08/18] Remove try/catch in drawing logic. Improve slf4j helper function. Some rename. --- .../main/kotlin/ru/nucodelabs/kfx/ext/FX.kt | 2 +- .../ru/nucodelabs/gem/app/GemApplication.kt | 2 +- .../gem/app/UncaughtExceptionHandler.kt | 8 +- .../java/ru/nucodelabs/gem/config/Logging.kt | 3 +- .../gem/fxmodel/ves/app/VesFxAppModel.kt | 4 +- .../ru/nucodelabs/gem/view/AlertsFactory.kt | 17 ++-- .../main/AnisotropyMainViewController.kt | 6 +- .../charts/CurvesPseudoSectionController.kt | 4 +- .../charts/MapPseudoSectionController.kt | 4 +- .../charts/MisfitStacksController.kt | 94 +++++++------------ .../charts/ModelSectionController.kt | 9 +- .../controller/charts/VesCurvesController.kt | 18 ++-- src/main/resources/ui.properties | 4 +- src/main/resources/ui_ru.properties | 3 + 14 files changed, 78 insertions(+), 100 deletions(-) diff --git a/kfx-utils/src/main/kotlin/ru/nucodelabs/kfx/ext/FX.kt b/kfx-utils/src/main/kotlin/ru/nucodelabs/kfx/ext/FX.kt index 1a30fa3b..5fef563d 100644 --- a/kfx-utils/src/main/kotlin/ru/nucodelabs/kfx/ext/FX.kt +++ b/kfx-utils/src/main/kotlin/ru/nucodelabs/kfx/ext/FX.kt @@ -211,7 +211,7 @@ fun Tooltip.noAutoHide() = apply { showDuration = Duration.INDEFINITE } fun Tooltip.noHideDelay() = apply { hideDelay = Duration.ZERO } -fun Tooltip.forCharts() = this.noDelay().noAutoHide().noHideDelay() +fun Tooltip.shownOnHover() = this.noDelay().noAutoHide().noHideDelay() /** * As PNG diff --git a/src/main/java/ru/nucodelabs/gem/app/GemApplication.kt b/src/main/java/ru/nucodelabs/gem/app/GemApplication.kt index 185470d5..7f5429e3 100644 --- a/src/main/java/ru/nucodelabs/gem/app/GemApplication.kt +++ b/src/main/java/ru/nucodelabs/gem/app/GemApplication.kt @@ -25,7 +25,7 @@ class GemApplication : GuiceApplication(AppModule()) { private val macOSHandledFiles: MutableList = mutableListOf() - val log = slf4j(this) + val log = slf4j() @Inject lateinit var alertsFactory: AlertsFactory diff --git a/src/main/java/ru/nucodelabs/gem/app/UncaughtExceptionHandler.kt b/src/main/java/ru/nucodelabs/gem/app/UncaughtExceptionHandler.kt index e2ee9f06..c4b0b21f 100644 --- a/src/main/java/ru/nucodelabs/gem/app/UncaughtExceptionHandler.kt +++ b/src/main/java/ru/nucodelabs/gem/app/UncaughtExceptionHandler.kt @@ -9,12 +9,16 @@ class UncaughtExceptionHandler( private val alertsFactory: AlertsFactory, ) : UncaughtExceptionHandler { - val log = slf4j(this) + val log = slf4j() override fun uncaughtException(t: Thread, e: Throwable) { log.error("Uncaught exception", e) if (Platform.isFxApplicationThread()) { - alertsFactory.uncaughtExceptionAlert(e).show() + if (e is UnsatisfiedLinkError) { + alertsFactory.unsatisfiedLinkErrorAlert(e).show() + } else { + alertsFactory.uncaughtExceptionAlert(e).show() + } } } } \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/gem/config/Logging.kt b/src/main/java/ru/nucodelabs/gem/config/Logging.kt index 7c8a1271..218a490f 100644 --- a/src/main/java/ru/nucodelabs/gem/config/Logging.kt +++ b/src/main/java/ru/nucodelabs/gem/config/Logging.kt @@ -1,5 +1,6 @@ package ru.nucodelabs.gem.config +import org.slf4j.Logger import org.slf4j.LoggerFactory -fun slf4j(instance: Any) = LoggerFactory.getLogger(instance::class.java)!! \ No newline at end of file +inline fun T.slf4j(): Logger = LoggerFactory.getLogger(T::class.java)!! \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/gem/fxmodel/ves/app/VesFxAppModel.kt b/src/main/java/ru/nucodelabs/gem/fxmodel/ves/app/VesFxAppModel.kt index 9d2b05a6..de3b20ef 100644 --- a/src/main/java/ru/nucodelabs/gem/fxmodel/ves/app/VesFxAppModel.kt +++ b/src/main/java/ru/nucodelabs/gem/fxmodel/ves/app/VesFxAppModel.kt @@ -17,7 +17,7 @@ class VesFxAppModel @Inject constructor( private val observableSection: ObservableSection, private val historyManager: HistoryManager
, private val initialModelService: InitialModelService, - private val metricsService: MetricsService, + private val metricsService: MetricsService ) { private val selectedIndex: Int by selectedIndexObservable::value @@ -64,7 +64,7 @@ class VesFxAppModel @Inject constructor( return metricsService.errorAvgMax(picket.effectiveExperimentalData, picket.modelData) } - fun targetFunction(): Double { + fun targetFunctionValue(): Double { return metricsService.targetFunctionValue(picket.effectiveExperimentalData, picket.modelData) } diff --git a/src/main/java/ru/nucodelabs/gem/view/AlertsFactory.kt b/src/main/java/ru/nucodelabs/gem/view/AlertsFactory.kt index 17471a3c..6c3331f1 100644 --- a/src/main/java/ru/nucodelabs/gem/view/AlertsFactory.kt +++ b/src/main/java/ru/nucodelabs/gem/view/AlertsFactory.kt @@ -11,24 +11,23 @@ import java.util.* class AlertsFactory @Inject constructor(private val uiProperties: ResourceBundle) { - val log = slf4j(this) + val log = slf4j() fun simpleAlert( title: String = uiProperties["error"], headerText: String = "", text: String, owner: Stage? = currentWindow() - ) = - Alert(Alert.AlertType.ERROR, text).apply { - this.title = title - this.headerText = headerText - initOwner(owner) - } + ) = Alert(Alert.AlertType.ERROR, text).apply { + this.title = title + this.headerText = headerText + initOwner(owner) + } fun uncaughtExceptionAlert(e: Throwable) = Alert(Alert.AlertType.ERROR, e.message).apply { title = uiProperties["error"] + headerText = uiProperties["saveAndRestart"] initOwner(currentWindow()) - headerText = "Сохраните важные данные и перезапустите программу" }.also { log.warn("Uncaught exception alert", e) } @JvmOverloads @@ -43,7 +42,7 @@ class AlertsFactory @Inject constructor(private val uiProperties: ResourceBundle fun unsatisfiedLinkErrorAlert(e: UnsatisfiedLinkError, owner: Stage? = currentWindow()): Alert = Alert(Alert.AlertType.ERROR, e.message).apply { title = uiProperties["noLib"] - headerText = uiProperties["unableToDrawChart"] + headerText = uiProperties["saveAndRestart"] initOwner(owner) }.also { log.warn("Unsatisfied link error", e) } diff --git a/src/main/java/ru/nucodelabs/gem/view/controller/anisotropy/main/AnisotropyMainViewController.kt b/src/main/java/ru/nucodelabs/gem/view/controller/anisotropy/main/AnisotropyMainViewController.kt index 2c693a5f..5fa49903 100644 --- a/src/main/java/ru/nucodelabs/gem/view/controller/anisotropy/main/AnisotropyMainViewController.kt +++ b/src/main/java/ru/nucodelabs/gem/view/controller/anisotropy/main/AnisotropyMainViewController.kt @@ -537,7 +537,7 @@ class AnisotropyMainViewController @Inject constructor( ρₐ = ${df.format(resistance)} Ω‧m Азимут = ${df.format(azimuth)} ° """.trimIndent() - return Tooltip(tooltipText).forCharts() + return Tooltip(tooltipText).shownOnHover() } @Suppress("unused") @@ -553,7 +553,7 @@ class AnisotropyMainViewController @Inject constructor( AB/2 = ${df.format(point.xValue)} m Отношение = ${df.format(point.yValue)} """.trimIndent() - ).forCharts() + ).shownOnHover() } @Suppress("unused") @@ -575,7 +575,7 @@ class AnisotropyMainViewController @Inject constructor( MN/2 = ${df.format(mn2)} m ρₐ = ${df.format(resistance)} Ω‧m """.trimIndent() - ).forCharts() + ).shownOnHover() } @FXML diff --git a/src/main/java/ru/nucodelabs/gem/view/controller/charts/CurvesPseudoSectionController.kt b/src/main/java/ru/nucodelabs/gem/view/controller/charts/CurvesPseudoSectionController.kt index 68bbe0bf..7a5f8cdc 100644 --- a/src/main/java/ru/nucodelabs/gem/view/controller/charts/CurvesPseudoSectionController.kt +++ b/src/main/java/ru/nucodelabs/gem/view/controller/charts/CurvesPseudoSectionController.kt @@ -12,8 +12,8 @@ import ru.nucodelabs.gem.fxmodel.ves.ObservableSection import ru.nucodelabs.geo.ves.ExperimentalData import ru.nucodelabs.geo.ves.calc.effectiveToSortedIndicesMapping import ru.nucodelabs.geo.ves.calc.graph.CurvesSectionGraphContext -import ru.nucodelabs.kfx.ext.forCharts import ru.nucodelabs.kfx.ext.installTooltips +import ru.nucodelabs.kfx.ext.shownOnHover import ru.nucodelabs.kfx.ext.toObservableList import java.net.URL import java.text.DecimalFormat @@ -74,6 +74,6 @@ class CurvesPseudoSectionController @Inject constructor( AB/2 = $ab2 m ρₐ = $res Ω‧m """.trimIndent() - ).forCharts() + ).shownOnHover() } } \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/gem/view/controller/charts/MapPseudoSectionController.kt b/src/main/java/ru/nucodelabs/gem/view/controller/charts/MapPseudoSectionController.kt index 92985b5a..c3e8f6d1 100644 --- a/src/main/java/ru/nucodelabs/gem/view/controller/charts/MapPseudoSectionController.kt +++ b/src/main/java/ru/nucodelabs/gem/view/controller/charts/MapPseudoSectionController.kt @@ -14,8 +14,8 @@ import ru.nucodelabs.gem.view.color.ColorMapper import ru.nucodelabs.gem.view.control.chart.InterpolationMap import ru.nucodelabs.geo.ves.calc.effectiveToSortedIndicesMapping import ru.nucodelabs.geo.ves.calc.xOfPicket -import ru.nucodelabs.kfx.ext.forCharts import ru.nucodelabs.kfx.ext.installTooltips +import ru.nucodelabs.kfx.ext.shownOnHover import ru.nucodelabs.kfx.ext.toObservableList import java.net.URL import java.text.DecimalFormat @@ -81,6 +81,6 @@ class MapPseudoSectionController @Inject constructor( AB/2 = ${decimalFormat.format(point.yValue)} m ρₐ = ${decimalFormat.format(point.extraValue)} Ω‧m """.trimIndent() - ).forCharts() + ).shownOnHover() } } diff --git a/src/main/java/ru/nucodelabs/gem/view/controller/charts/MisfitStacksController.kt b/src/main/java/ru/nucodelabs/gem/view/controller/charts/MisfitStacksController.kt index 14dd248b..b8b0c600 100644 --- a/src/main/java/ru/nucodelabs/gem/view/controller/charts/MisfitStacksController.kt +++ b/src/main/java/ru/nucodelabs/gem/view/controller/charts/MisfitStacksController.kt @@ -1,11 +1,7 @@ package ru.nucodelabs.gem.view.controller.charts import jakarta.inject.Inject -import javafx.beans.property.ObjectProperty -import javafx.beans.property.SimpleObjectProperty import javafx.beans.value.ObservableObjectValue -import javafx.collections.FXCollections -import javafx.collections.ObservableList import javafx.fxml.FXML import javafx.scene.chart.LineChart import javafx.scene.chart.NumberAxis @@ -17,15 +13,13 @@ import javafx.scene.image.Image import javafx.scene.image.ImageView import javafx.scene.layout.VBox import ru.nucodelabs.gem.fxmodel.ves.app.VesFxAppModel -import ru.nucodelabs.gem.view.AlertsFactory import ru.nucodelabs.gem.view.control.chart.log.LogarithmicAxis import ru.nucodelabs.geo.ves.Picket import ru.nucodelabs.geo.ves.calc.graph.experimentalCurve import ru.nucodelabs.kfx.core.AbstractViewController import ru.nucodelabs.kfx.ext.Point -import ru.nucodelabs.kfx.ext.forCharts import ru.nucodelabs.kfx.ext.line -import ru.nucodelabs.kfx.ext.observableListOf +import ru.nucodelabs.kfx.ext.shownOnHover import java.math.RoundingMode import java.net.URL import java.text.DecimalFormat @@ -48,11 +42,9 @@ const val ERROR_FORMULA_IMAGE_PATH = "/img/error.png" class MisfitStacksController @Inject constructor( private val picketObservable: ObservableObjectValue, - private val alertsFactory: AlertsFactory, private val decimalFormat: DecimalFormat, private val appModel: VesFxAppModel, ) : AbstractViewController() { - private val dataProperty = emptyData() @FXML private lateinit var targetFunctionText: Label @@ -75,65 +67,53 @@ class MisfitStacksController @Inject constructor( private val picket: Picket get() = picketObservable.get()!! + private val format4FracDigits = DecimalFormat("#.####").apply { roundingMode = RoundingMode.HALF_UP } + override fun initialize(location: URL, resources: ResourceBundle) { picketObservable.addListener { _, _, newValue: Picket? -> if (newValue != null) { update() } } - lineChart.dataProperty().bind(dataProperty) installTooltipForTerms() } private fun update() { - var misfitStacksSeriesList: MutableList> = ArrayList() - try { - val misfits = appModel.misfits() - val expPoints = experimentalCurve(picket) - - misfitStacksSeriesList = observableListOf() - - if (picket.effectiveExperimentalData.isNotEmpty() && picket.modelData.isNotEmpty()) { - check(misfits.size == expPoints.size) - - for ((index, expPoint) in expPoints.withIndex()) { - misfitStacksSeriesList += line( - Point(expPoint.x as Number, 0.0 as Number), - Point(expPoint.x as Number, misfits[index] as Number) - ) - } - - val targetFunction = appModel.targetFunction() - val (misfitsAvg, misfitsMax) = appModel.misfitsAvgMax() - val (errorAvg, errorMax) = appModel.errorAvgMax() - - val dfTwo = DecimalFormat("#.##").apply { roundingMode = RoundingMode.HALF_UP } - val dfFour = DecimalFormat("#.####").apply { roundingMode = RoundingMode.HALF_UP } - - targetFunctionText.text = - "целевая функция: f = ${dfFour.format(targetFunction)}" - misfitText.text = - "отклонение: avg = ${dfTwo.format(misfitsAvg)}%, max = ${dfTwo.format(misfitsMax)}%" - errorText.text = - "погрешность: avg = ${dfTwo.format(errorAvg)}% , max = ${dfTwo.format(errorMax)}%" + val misfitStacksSeriesList: MutableList> = ArrayList() + val misfits = appModel.misfits() + val expPoints = experimentalCurve(picket) + + if (picket.effectiveExperimentalData.isNotEmpty() && picket.modelData.isNotEmpty()) { + check(misfits.size == expPoints.size) + + for ((index, expPoint) in expPoints.withIndex()) { + misfitStacksSeriesList += line( + Point(expPoint.x as Number, 0.0 as Number), + Point(expPoint.x as Number, misfits[index] as Number) + ) } - } catch (e: UnsatisfiedLinkError) { - alertsFactory.unsatisfiedLinkErrorAlert(e, stage).show() - } catch (e: IllegalStateException) { - alertsFactory.simpleExceptionAlert(e, stage).show() + val targetFunction = appModel.targetFunctionValue() + val (misfitsAvg, misfitsMax) = appModel.misfitsAvgMax() + val (errorAvg, errorMax) = appModel.errorAvgMax() + + targetFunctionText.text = + "целевая функция: f = ${format4FracDigits.format(targetFunction)}" + misfitText.text = + "отклонение: avg = ${decimalFormat.format(misfitsAvg)}%, max = ${decimalFormat.format(misfitsMax)}%" + errorText.text = + "погрешность: avg = ${decimalFormat.format(errorAvg)}% , max = ${decimalFormat.format(errorMax)}%" } - dataProperty.get().clear() - dataProperty.get() += misfitStacksSeriesList + lineChart.data.setAll(misfitStacksSeriesList) colorizeMisfitStacksSeries() installTooltips() } private fun installTooltips() { - dataProperty.get().forEach { + lineChart.data.forEach { val text = "${decimalFormat.format(it.data[1].yValue)}%" - Tooltip.install(it.node, Tooltip(text).forCharts()) - it.data.forEach { p -> Tooltip.install(p.node, Tooltip(text).forCharts()) } + Tooltip.install(it.node, Tooltip(text).shownOnHover()) + it.data.forEach { p -> Tooltip.install(p.node, Tooltip(text).shownOnHover()) } } } @@ -145,7 +125,7 @@ class MisfitStacksController @Inject constructor( tooltipForTargetFunction.text = TARGET_FUNCTION_DESCRIPTION tooltipForTargetFunction.graphic = ImageView(imageForTargetFunction) tooltipForTargetFunction.contentDisplay = ContentDisplay.BOTTOM - Tooltip.install(targetFunctionText, tooltipForTargetFunction.forCharts()) + Tooltip.install(targetFunctionText, tooltipForTargetFunction.shownOnHover()) val imageForMisfit = Image( javaClass.getResourceAsStream(MISFIT_FORMULA_IMAGE_PATH) @@ -154,7 +134,7 @@ class MisfitStacksController @Inject constructor( tooltipForMisfit.text = MISFIT_DESCRIPTION tooltipForMisfit.graphic = ImageView(imageForMisfit) tooltipForMisfit.contentDisplay = ContentDisplay.BOTTOM - Tooltip.install(misfitText, tooltipForMisfit.forCharts()) + Tooltip.install(misfitText, tooltipForMisfit.shownOnHover()) val imageForError = Image( javaClass.getResourceAsStream(ERROR_FORMULA_IMAGE_PATH) @@ -163,11 +143,11 @@ class MisfitStacksController @Inject constructor( tooltipForError.text = ERROR_DESCRIPTION tooltipForError.graphic = ImageView(imageForError) tooltipForError.contentDisplay = ContentDisplay.BOTTOM - Tooltip.install(errorText, tooltipForError.forCharts()) + Tooltip.install(errorText, tooltipForError.shownOnHover()) } private fun colorizeMisfitStacksSeries() { - val data = dataProperty.get() + val data = lineChart.data for (series in data) { val nonZeroPoint = series.data[1] if (abs(nonZeroPoint.yValue.toDouble()) < 100.0) { @@ -178,10 +158,4 @@ class MisfitStacksController @Inject constructor( } } } -} - -fun emptyData(): ObjectProperty>> = SimpleObjectProperty( - FXCollections.observableArrayList( - ArrayList() - ) -) +} \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/gem/view/controller/charts/ModelSectionController.kt b/src/main/java/ru/nucodelabs/gem/view/controller/charts/ModelSectionController.kt index 901e5f11..4ea6344a 100644 --- a/src/main/java/ru/nucodelabs/gem/view/controller/charts/ModelSectionController.kt +++ b/src/main/java/ru/nucodelabs/gem/view/controller/charts/ModelSectionController.kt @@ -96,15 +96,14 @@ class ModelSectionController @Inject constructor( setupXAxisMarks() setupYAxisBounds() - chart.data.clear() - if (observableSection.pickets.isEmpty()) { + chart.data.clear() return } val lowerBoundZ = zWithVirtualLastLayers().minOfOrNull { it.minOrNull() ?: 0.0 } ?: 0.0 - val dataToAdd = ArrayList>() + val chartData = ArrayList>() for ((index, bounds) in observableSection.asSection().picketsBounds().withIndex()) { val picket = observableSection.pickets[index] if (picket.modelData.isEmpty()) { @@ -137,11 +136,11 @@ class ModelSectionController @Inject constructor( ) ) - dataToAdd += series + chartData += series } } - chart.data += dataToAdd + chart.data.setAll(chartData) } private fun setupXAxisMarks() { diff --git a/src/main/java/ru/nucodelabs/gem/view/controller/charts/VesCurvesController.kt b/src/main/java/ru/nucodelabs/gem/view/controller/charts/VesCurvesController.kt index 0739690d..b59ebda0 100644 --- a/src/main/java/ru/nucodelabs/gem/view/controller/charts/VesCurvesController.kt +++ b/src/main/java/ru/nucodelabs/gem/view/controller/charts/VesCurvesController.kt @@ -23,7 +23,6 @@ import ru.nucodelabs.gem.app.pref.PNG_FILES_DIR import ru.nucodelabs.gem.config.Name import ru.nucodelabs.gem.config.Style import ru.nucodelabs.gem.fxmodel.ves.ObservableSection -import ru.nucodelabs.gem.view.AlertsFactory import ru.nucodelabs.gem.view.control.chart.log.LogarithmicAxis import ru.nucodelabs.gem.view.control.chart.log.LogarithmicChartNavigationSupport import ru.nucodelabs.gem.view.controller.AbstractController @@ -53,7 +52,6 @@ private const val ZOOM_DELTA_LOG = 0.05 class VesCurvesController @Inject constructor( private val picketObservable: ObservableObjectValue, private val _picketIndex: IntegerProperty, - private val alertsFactory: AlertsFactory, private val observableSection: ObservableSection, private val historyManager: HistoryManager
, private val decimalFormat: DecimalFormat, @@ -268,13 +266,11 @@ class VesCurvesController @Inject constructor( private fun updateTheoreticalCurve() { val theorCurveSeries = Series() - try { - theorCurveSeries.data.addAll( - theoreticalCurve(picket, forwardSolver).map { (x, y) -> Data(x as Number, y as Number) } - ) - } catch (e: UnsatisfiedLinkError) { - alertsFactory.unsatisfiedLinkErrorAlert(e, stage) - } + + theorCurveSeries.data.addAll( + theoreticalCurve(picket, forwardSolver).map { (x, y) -> Data(x as Number, y as Number) } + ) + theorCurveSeries.name = uiProperties["theorCurve"] dataProperty.get()[THEOR_CURVE_SERIES_INDEX] = theorCurveSeries } @@ -384,7 +380,7 @@ class VesCurvesController @Inject constructor( min ρₐ = $yLower max ρₐ = $yUpper """.trimIndent() - ).forCharts() + ).shownOnHover() } THEOR_CURVE_SERIES_INDEX -> { @@ -406,7 +402,7 @@ class VesCurvesController @Inject constructor( max ρₐ = $yUpper ρₐ (теор.) = ${decimalFormat.format(theorRes)} Ω‧m """.trimIndent() - ).forCharts() + ).shownOnHover() } else -> null diff --git a/src/main/resources/ui.properties b/src/main/resources/ui.properties index 2bfed1e1..740c67c5 100644 --- a/src/main/resources/ui.properties +++ b/src/main/resources/ui.properties @@ -47,10 +47,12 @@ noFileTitle=Welcome to GEM noFileText=You can open SONET-compatible file for VES interpretation openEXPFile=Open EXP file useSystemMenu=Use system menu -unableToDrawChart=Unable to draw chart +# Errors noLib=Library not found fileError=An error occurred while opening the file error=Error +saveAndRestart=Save data and restart application + compatibilityMode=Compatibility Mode EXPSTTMismatch=STT and EXP files lines counts are unequal minimalDataWillBeDisplayed=Minimal possible amount of data will be displayed diff --git a/src/main/resources/ui_ru.properties b/src/main/resources/ui_ru.properties index c3a85b49..7fca5971 100644 --- a/src/main/resources/ui_ru.properties +++ b/src/main/resources/ui_ru.properties @@ -61,10 +61,13 @@ noFileTitle=Добро пожаловать в GEM noFileText=Вы можете открыть SONET-совместимый файл для интерпретации ВЭЗ openEXPFile=Открыть EXP-файл useSystemMenu=Использовать системное меню +# Erros unableToDrawChart=Невозможно отрисовать график noLib=Отсутствует библиотека fileError=Произошла ошибка при открытии файла error=Ошибка +saveAndRestart=Сохраните важные данные и перезапустите программу + compatibilityMode=Режим совместимости EXPSTTMismatch=STT и EXP содержат разное количество строк minimalDataWillBeDisplayed=Будет отображено минимальное возможное количество данных From a97e49d74434bf99ef7cbf7bec8c21a368d829d9 Mon Sep 17 00:00:00 2001 From: Vadim Mostovoy Date: Tue, 30 Sep 2025 16:53:03 +0700 Subject: [PATCH 09/18] Data Model refactor. Add Mutable/ReadOnly Interfaces. - Improve AddExperimentalData screen with validation. - Improve Experimental Data and Model Tables with localized validation messages. - Fix CanvasRenderPolygonChart.kt - Add ObservablePicket, ObservableSection models --- .../main/kotlin/ru/nucodelabs/util/Result.kt | 9 +- data/gem_json_examples/burm.section.json | 8 +- .../nucodelabs/kfx/observable/Constrained.kt | 25 +++ .../ru/nucodelabs/gem/config/AppModule.java | 4 +- .../gem/file/dto/mapper/DtoMapper.java | 2 +- .../fxmodel/ves/ObservableExperimentalData.kt | 62 +++-- .../gem/fxmodel/ves/ObservableModelLayer.kt | 24 +- .../ves/observable/ObservablePicket.kt | 101 +++++++++ .../ves/observable/ObservableSection.kt | 10 + .../ru/nucodelabs/gem/view/AlertsFactory.kt | 7 + .../control/chart/CanvasRenderPolygonChart.kt | 4 + .../charts/MapPseudoSectionController.kt | 4 +- .../controller/charts/VesCurvesController.kt | 13 +- .../main/AddExperimentalDataController.kt | 147 +++++++----- .../controller/main/MainViewController.kt | 9 +- .../tables/ExperimentalTableController.kt | 150 +++++-------- .../controller/tables/ModelTableController.kt | 140 +++++------- .../ru/nucodelabs/geo/anisotropy/Signal.kt | 41 ++-- .../ru/nucodelabs/geo/ves/ExperimentalData.kt | 136 +++++++++-- .../nucodelabs/geo/ves/ExperimentalDataSet.kt | 57 +++++ .../geo/ves/InvalidPropertiesException.kt | 11 + .../ru/nucodelabs/geo/ves/ModelDataSet.kt | 6 + .../java/ru/nucodelabs/geo/ves/ModelLayer.kt | 82 ++++++- src/main/java/ru/nucodelabs/geo/ves/Picket.kt | 211 +++++++----------- .../java/ru/nucodelabs/geo/ves/Section.kt | 6 +- .../geo/ves/SectionExperimentalDataSet.kt | 5 + .../geo/ves/calc/ExperimentalDataFunctions.kt | 9 +- .../geo/ves/calc/ModelLayerFunctions.kt | 7 +- .../nucodelabs/geo/ves/calc/Normalization.kt | 6 +- .../geo/ves/calc/PicketFunctions.kt | 17 +- .../geo/ves/calc/SectionFunctions.kt | 22 +- .../geo/ves/calc/{Ves.kt => VesFunctions.kt} | 1 + .../ves/calc/adapter/ForwardSolverAdapter.kt | 9 +- .../calc/graph/CurvesSectionGraphContext.kt | 50 +++-- .../graph/MathVesNativeMisfitsFunction.kt | 9 +- .../geo/ves/calc/graph/MisfitsFunction.kt | 9 +- .../geo/ves/calc/graph/VesCurvesFunctions.kt | 32 +-- .../initialModel/MultiLayerInitialModel.kt | 6 +- .../calc/initialModel/SimpleInitialModel.java | 24 +- .../geo/ves/calc/inverse/InverseSolver.java | 21 +- .../ves/calc/inverse/InverseSolverAdapter.kt | 7 +- .../ves/calc/inverse/func/FunctionValue.java | 21 +- .../gem/view/controller/charts/VesCurves.fxml | 1 + .../controller/main/AddExperimentalData.fxml | 13 +- src/main/resources/ui.properties | 10 + src/main/resources/ui_ru.properties | 18 +- .../algorithms/InverseSolverTest.java | 8 +- .../gem/file/dto/mapper/DtoMapperTest.java | 6 +- .../geo/ves/ExperimentalDataTest.kt | 6 +- .../java/ru/nucodelabs/geo/ves/PicketTest.kt | 2 +- .../geo/ves/TableConversionKtTest.kt | 9 +- 51 files changed, 1016 insertions(+), 581 deletions(-) create mode 100644 kfx-utils/src/main/kotlin/ru/nucodelabs/kfx/observable/Constrained.kt create mode 100644 src/main/java/ru/nucodelabs/gem/fxmodel/ves/observable/ObservablePicket.kt create mode 100644 src/main/java/ru/nucodelabs/gem/fxmodel/ves/observable/ObservableSection.kt create mode 100644 src/main/java/ru/nucodelabs/geo/ves/ExperimentalDataSet.kt create mode 100644 src/main/java/ru/nucodelabs/geo/ves/InvalidPropertiesException.kt create mode 100644 src/main/java/ru/nucodelabs/geo/ves/ModelDataSet.kt create mode 100644 src/main/java/ru/nucodelabs/geo/ves/SectionExperimentalDataSet.kt rename src/main/java/ru/nucodelabs/geo/ves/calc/{Ves.kt => VesFunctions.kt} (94%) diff --git a/common-utils/src/main/kotlin/ru/nucodelabs/util/Result.kt b/common-utils/src/main/kotlin/ru/nucodelabs/util/Result.kt index 5f3a2698..3ca19bcb 100644 --- a/common-utils/src/main/kotlin/ru/nucodelabs/util/Result.kt +++ b/common-utils/src/main/kotlin/ru/nucodelabs/util/Result.kt @@ -2,13 +2,16 @@ package ru.nucodelabs.util sealed interface Result -class Ok(val value: T) : Result +@JvmInline +value class Ok(val value: T) : Result -class Err(val error: E) : Result +@JvmInline +value class Err(val error: E) : Result fun T.toOkResult(): Result = Ok(this) fun E.toErrorResult(): Result = Err(this) +@Suppress("unused") inline fun Result.okOrThrow( mapError: (E) -> Throwable = { IllegalStateException(it.toString()) } ): T { @@ -18,11 +21,13 @@ inline fun Result.okOrThrow( } } +@Suppress("unused") inline fun Result.ifError(action: (E) -> Unit): Result { if (this is Err) action(error) return this } +@Suppress("unused") inline fun Result.ifOk(action: (T) -> Unit): Result { if (this is Ok) action(value) return this diff --git a/data/gem_json_examples/burm.section.json b/data/gem_json_examples/burm.section.json index da37cb19..c522d53c 100644 --- a/data/gem_json_examples/burm.section.json +++ b/data/gem_json_examples/burm.section.json @@ -321,7 +321,7 @@ "isFixedResistance": false }, { - "power": "NaN", + "power": "0.0", "resistance": 1414.69, "isFixedPower": false, "isFixedResistance": false @@ -652,7 +652,7 @@ "isFixedResistance": false }, { - "power": "NaN", + "power": "0.0", "resistance": 1882.95, "isFixedPower": false, "isFixedResistance": false @@ -974,7 +974,7 @@ "isFixedResistance": false }, { - "power": "NaN", + "power": "0.0", "resistance": 1882.95, "isFixedPower": false, "isFixedResistance": false @@ -1272,7 +1272,7 @@ "isFixedResistance": false }, { - "power": "NaN", + "power": "0.0", "resistance": 1062.88, "isFixedPower": false, "isFixedResistance": false diff --git a/kfx-utils/src/main/kotlin/ru/nucodelabs/kfx/observable/Constrained.kt b/kfx-utils/src/main/kotlin/ru/nucodelabs/kfx/observable/Constrained.kt new file mode 100644 index 00000000..80f39086 --- /dev/null +++ b/kfx-utils/src/main/kotlin/ru/nucodelabs/kfx/observable/Constrained.kt @@ -0,0 +1,25 @@ +package ru.nucodelabs.kfx.observable + +import javafx.beans.property.SimpleDoubleProperty +import javafx.beans.property.SimpleObjectProperty + + +class ConstrainedObjectProperty( + value: T?, + val isValid: (T?) -> Boolean +) : SimpleObjectProperty(value) { + + override fun set(value: T?) { + if (isValid(value)) super.set(value) + } +} + +class ConstrainedDoubleProperty( + value: Double, + val isValid: (Double) -> Boolean +) : SimpleDoubleProperty(value) { + + override fun set(value: Double) { + if (isValid(value)) super.set(value) + } +} \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/gem/config/AppModule.java b/src/main/java/ru/nucodelabs/gem/config/AppModule.java index d821e2e7..fe6c689a 100644 --- a/src/main/java/ru/nucodelabs/gem/config/AppModule.java +++ b/src/main/java/ru/nucodelabs/gem/config/AppModule.java @@ -186,7 +186,7 @@ public String toString(Double object) { try { return decimalFormat.format(object); } catch (Exception e) { - return ""; + return null; } } @@ -195,7 +195,7 @@ public Double fromString(String string) { try { return decimalFormat.parse(string).doubleValue(); } catch (ParseException e) { - return Double.NaN; + return null; } } }; diff --git a/src/main/java/ru/nucodelabs/gem/file/dto/mapper/DtoMapper.java b/src/main/java/ru/nucodelabs/gem/file/dto/mapper/DtoMapper.java index b70828fb..76820546 100644 --- a/src/main/java/ru/nucodelabs/gem/file/dto/mapper/DtoMapper.java +++ b/src/main/java/ru/nucodelabs/gem/file/dto/mapper/DtoMapper.java @@ -18,7 +18,7 @@ public abstract class DtoMapper { @Mapping( target = "resistanceApparent", - defaultExpression = "java(ru.nucodelabs.geo.ves.calc.VesKt.rhoA(dto.getAb2(), dto.getMn2(), dto.getAmperage(), dto.getVoltage()))" + defaultExpression = "java(ru.nucodelabs.geo.ves.calc.VesFunctions.rhoA(dto.getAb2(), dto.getMn2(), dto.getAmperage(), dto.getVoltage()))" ) @Mapping(target = "errorResistanceApparent", defaultExpression = "java(ru.nucodelabs.geo.anisotropy.DefaultValues.DEFAULT_ERROR)") @Mapping(target = "isHidden", source = "hidden", defaultValue = "false") diff --git a/src/main/java/ru/nucodelabs/gem/fxmodel/ves/ObservableExperimentalData.kt b/src/main/java/ru/nucodelabs/gem/fxmodel/ves/ObservableExperimentalData.kt index 9b377eac..f15d56a4 100644 --- a/src/main/java/ru/nucodelabs/gem/fxmodel/ves/ObservableExperimentalData.kt +++ b/src/main/java/ru/nucodelabs/gem/fxmodel/ves/ObservableExperimentalData.kt @@ -1,7 +1,10 @@ package ru.nucodelabs.gem.fxmodel.ves import javafx.beans.property.SimpleBooleanProperty -import javafx.beans.property.SimpleDoubleProperty +import ru.nucodelabs.geo.ves.ExperimentalData +import ru.nucodelabs.geo.ves.MutableExperimentalSignal +import ru.nucodelabs.geo.ves.ReadOnlyExperimentalSignal +import ru.nucodelabs.kfx.observable.ConstrainedDoubleProperty import tornadofx.getValue import tornadofx.setValue @@ -13,32 +16,59 @@ class ObservableExperimentalData( resistanceApparent: Double, errorResistanceApparent: Double, isHidden: Boolean -) { - private val ab2Property = SimpleDoubleProperty(ab2) +) : MutableExperimentalSignal { + private val ab2Property = ConstrainedDoubleProperty( + ab2, + ExperimentalData::isValidDistance + ) + fun ab2Property() = ab2Property - var ab2 by ab2Property + override var ab2 by ab2Property + + private val mn2Property = ConstrainedDoubleProperty( + mn2, + ExperimentalData::isValidDistance + ) - private val mn2Property = SimpleDoubleProperty(mn2) fun mn2Property() = mn2Property - var mn2 by mn2Property + override var mn2 by mn2Property + + private val amperageProperty = ConstrainedDoubleProperty( + amperage, + ExperimentalData::isValidAmperage + ) - private val amperageProperty = SimpleDoubleProperty(amperage) fun amperageProperty() = amperageProperty - var amperage by amperageProperty + override var amperage by amperageProperty + + private val voltageProperty = ConstrainedDoubleProperty( + voltage, + ExperimentalData::isValidVoltage + ) - private val voltageProperty = SimpleDoubleProperty(voltage) fun voltageProperty() = voltageProperty - var voltage by voltageProperty + override var voltage by voltageProperty + + private val resistanceApparentProperty = ConstrainedDoubleProperty( + resistanceApparent, + ExperimentalData::isValidResistApparent + ) - private val resistanceApparentProperty = SimpleDoubleProperty(resistanceApparent) fun resistanceApparentProperty() = resistanceApparentProperty - var resistanceApparent by resistanceApparentProperty + override var resistanceApparent by resistanceApparentProperty + + private val errorResistanceApparentProperty = ConstrainedDoubleProperty( + errorResistanceApparent, + ExperimentalData::isValidErrResistApparent + ) - private val errorResistanceApparentProperty = SimpleDoubleProperty(errorResistanceApparent) fun errorResistanceApparentProperty() = errorResistanceApparentProperty - var errorResistanceApparent by errorResistanceApparentProperty + override var errorResistanceApparent by errorResistanceApparentProperty private val hiddenProperty = SimpleBooleanProperty(isHidden) fun hiddenProperty() = hiddenProperty - var isHidden by hiddenProperty -} \ No newline at end of file + override var isHidden by hiddenProperty +} + +fun ReadOnlyExperimentalSignal.toObservable() = + ObservableExperimentalData(ab2, mn2, amperage, voltage, resistanceApparent, errorResistanceApparent, isHidden) \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/gem/fxmodel/ves/ObservableModelLayer.kt b/src/main/java/ru/nucodelabs/gem/fxmodel/ves/ObservableModelLayer.kt index 7ee38089..2f2339ae 100644 --- a/src/main/java/ru/nucodelabs/gem/fxmodel/ves/ObservableModelLayer.kt +++ b/src/main/java/ru/nucodelabs/gem/fxmodel/ves/ObservableModelLayer.kt @@ -1,7 +1,10 @@ package ru.nucodelabs.gem.fxmodel.ves import javafx.beans.property.SimpleBooleanProperty -import javafx.beans.property.SimpleDoubleProperty +import ru.nucodelabs.geo.ves.ModelLayer +import ru.nucodelabs.geo.ves.MutableModelLayer +import ru.nucodelabs.geo.ves.ReadOnlyModelLayer +import ru.nucodelabs.kfx.observable.ConstrainedDoubleProperty import tornadofx.getValue import tornadofx.setValue @@ -10,20 +13,23 @@ class ObservableModelLayer( resistance: Double, isFixedPower: Boolean, isFixedResistance: Boolean -) { - private val powerProperty = SimpleDoubleProperty(power) +) : MutableModelLayer { + private val powerProperty = ConstrainedDoubleProperty(power, ModelLayer::isValidPower) fun powerProperty() = powerProperty - var power by powerProperty + override var power by powerProperty - private val resistanceProperty = SimpleDoubleProperty(resistance) + private val resistanceProperty = ConstrainedDoubleProperty(resistance, ModelLayer::isValidResist) fun resistanceProperty() = resistanceProperty - var resistance by resistanceProperty + override var resistance by resistanceProperty private val fixedPowerProperty = SimpleBooleanProperty(isFixedPower) fun fixedPowerProperty() = fixedPowerProperty - var isFixedPower by fixedPowerProperty + override var isFixedPower by fixedPowerProperty private val fixedResistanceProperty = SimpleBooleanProperty(isFixedResistance) fun fixedResistanceProperty() = fixedResistanceProperty - var isFixedResistance by fixedResistanceProperty -} \ No newline at end of file + override var isFixedResistance by fixedResistanceProperty +} + +fun ReadOnlyModelLayer.toObservable() = + ObservableModelLayer(power, resistance, isFixedPower, isFixedResistance) \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/gem/fxmodel/ves/observable/ObservablePicket.kt b/src/main/java/ru/nucodelabs/gem/fxmodel/ves/observable/ObservablePicket.kt new file mode 100644 index 00000000..1282e33e --- /dev/null +++ b/src/main/java/ru/nucodelabs/gem/fxmodel/ves/observable/ObservablePicket.kt @@ -0,0 +1,101 @@ +package ru.nucodelabs.gem.fxmodel.ves.observable + +import javafx.beans.property.DoubleProperty +import javafx.beans.property.SimpleDoubleProperty +import javafx.beans.property.SimpleStringProperty +import javafx.beans.property.StringProperty +import javafx.collections.FXCollections +import javafx.collections.ListChangeListener +import javafx.collections.ObservableList +import ru.nucodelabs.gem.fxmodel.ves.ObservableExperimentalData +import ru.nucodelabs.gem.fxmodel.ves.ObservableModelLayer +import ru.nucodelabs.geo.ves.* +import ru.nucodelabs.geo.ves.calc.orderByDistances +import ru.nucodelabs.kfx.observable.ConstrainedDoubleProperty +import tornadofx.getValue +import tornadofx.setValue + +class ObservablePicket : MutableExperimentalDataSet, ModelDataSet { + val nameProperty: StringProperty = SimpleStringProperty(Picket.DEFAULT_NAME) + var name by nameProperty + fun nameProperty() = nameProperty + + val rawExperimentalData: ObservableList = + FXCollections.observableArrayList { experimentalData -> + arrayOf( + experimentalData.ab2Property(), + experimentalData.mn2Property(), + experimentalData.amperageProperty(), + experimentalData.voltageProperty(), + experimentalData.resistanceApparentProperty(), + experimentalData.errorResistanceApparentProperty() + ) + } + + override val modelData: ObservableList = + FXCollections.observableArrayList().apply { + addListener(ListChangeListener { change -> + while (change.next()) { + if (change.wasAdded() && !Picket.isValidModelDataSize(change.list.size)) { + throw InvalidPropertiesException( + listOf( + InvalidPropertyValue("modelData.size", "", change.list.size) + ) + ) + } + } + }) + } + + val offsetXProperty: DoubleProperty = ConstrainedDoubleProperty(Picket.DEFAULT_X_OFFSET, Picket::isValidOffsetX) + override var offsetX by offsetXProperty + fun offsetXProperty() = offsetXProperty + + val modelZProperty: DoubleProperty = SimpleDoubleProperty(Picket.DEFAULT_Z) + override var modelZ by modelZProperty + fun modelZProperty() = modelZProperty + + val commentProperty: StringProperty = SimpleStringProperty(Picket.DEFAULT_COMMENT) + var comment by commentProperty + fun commentProperty() = commentProperty + + override val sortedExperimentalData: List = + rawExperimentalData.sorted(orderByDistances()).apply { + addListener(ListChangeListener { change -> + while (change.next()) { + when { + change.wasUpdated() -> handleExperimentalDataUpdate(change.list) + change.wasAdded() -> handleExperimentalDataUpdate(change.list) + change.wasReplaced() -> handleExperimentalDataUpdate(change.list) + } + } + }) + } + + private fun handleExperimentalDataUpdate(list: List) { + hideExtraSignalsInPlace(list) + } + + override val effectiveExperimentalData: List + get() = sortedExperimentalData.filter { !it.isHidden } + + override fun addSignal(signal: ObservableExperimentalData) { + rawExperimentalData.add(signal) + } + + override fun addSignals(signals: Iterable) { + rawExperimentalData.addAll(signals) + } + + override fun removeSignal(signal: ObservableExperimentalData) { + rawExperimentalData.remove(signal) + } + + override fun removeSignals(signals: Iterable) { + rawExperimentalData.removeAll(signals) + } + + override fun edit(index: Int, mutate: MutableExperimentalSignal.() -> Unit) { + rawExperimentalData[index].mutate() + } +} \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/gem/fxmodel/ves/observable/ObservableSection.kt b/src/main/java/ru/nucodelabs/gem/fxmodel/ves/observable/ObservableSection.kt new file mode 100644 index 00000000..05579071 --- /dev/null +++ b/src/main/java/ru/nucodelabs/gem/fxmodel/ves/observable/ObservableSection.kt @@ -0,0 +1,10 @@ +package ru.nucodelabs.gem.fxmodel.ves.observable + +import javafx.collections.FXCollections +import javafx.collections.ObservableList +import ru.nucodelabs.geo.ves.SectionExperimentalDataSet + +class ObservableSection : SectionExperimentalDataSet { + val pickets: ObservableList = FXCollections.observableArrayList() + override fun pickets() = pickets +} \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/gem/view/AlertsFactory.kt b/src/main/java/ru/nucodelabs/gem/view/AlertsFactory.kt index 6c3331f1..4ff3df86 100644 --- a/src/main/java/ru/nucodelabs/gem/view/AlertsFactory.kt +++ b/src/main/java/ru/nucodelabs/gem/view/AlertsFactory.kt @@ -60,4 +60,11 @@ class AlertsFactory @Inject constructor(private val uiProperties: ResourceBundle initOwner(owner) }.also { log.warn("Validation violations alert: $message") } } + + fun invalidInputAlert(message: String, owner: Stage? = currentWindow()): Alert { + return Alert(Alert.AlertType.WARNING, message).apply { + title = uiProperties["invalidInput"] + initOwner(owner) + } + } } \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/gem/view/control/chart/CanvasRenderPolygonChart.kt b/src/main/java/ru/nucodelabs/gem/view/control/chart/CanvasRenderPolygonChart.kt index 8fb13d10..48fea451 100644 --- a/src/main/java/ru/nucodelabs/gem/view/control/chart/CanvasRenderPolygonChart.kt +++ b/src/main/java/ru/nucodelabs/gem/view/control/chart/CanvasRenderPolygonChart.kt @@ -10,6 +10,7 @@ import javafx.scene.chart.ValueAxis import javafx.scene.paint.Color import javafx.util.StringConverter import ru.nucodelabs.gem.view.color.ColorMapper +import ru.nucodelabs.kfx.ext.clear import tornadofx.getValue import tornadofx.setValue @@ -27,6 +28,8 @@ class CanvasRenderPolygonChart @JvmOverloads constructor( addListener { _, old, new -> render(backgroundCanvas) } } var extraValueFormatter: StringConverter? by extraValueFormatterProperty + + @Suppress("unused") fun extraValueFormatterProperty(): ObjectProperty?> = extraValueFormatterProperty private val extraValueVisibleProperty = SimpleBooleanProperty(false).apply { @@ -44,6 +47,7 @@ class CanvasRenderPolygonChart @JvmOverloads constructor( private fun render(canvas: Canvas) { val graphics = canvas.graphicsContext2D + canvas.clear() data.forEach { series -> if (series.data.isEmpty()) return@forEach val (xPoints, yPoints) = toPolygon(series) diff --git a/src/main/java/ru/nucodelabs/gem/view/controller/charts/MapPseudoSectionController.kt b/src/main/java/ru/nucodelabs/gem/view/controller/charts/MapPseudoSectionController.kt index c3e8f6d1..ae69444d 100644 --- a/src/main/java/ru/nucodelabs/gem/view/controller/charts/MapPseudoSectionController.kt +++ b/src/main/java/ru/nucodelabs/gem/view/controller/charts/MapPseudoSectionController.kt @@ -52,12 +52,12 @@ class MapPseudoSectionController @Inject constructor( val section = observableSection.asSection() val data: MutableList> = mutableListOf() - for (picket in section.pickets) { + for ((picketIdx, picket) in section.pickets.withIndex()) { val indexMapping = picket.effectiveToSortedIndicesMapping() for ((index, expData) in picket.effectiveExperimentalData.withIndex()) { data.add( Data( - section.xOfPicket(picket) as Number, + section.xOfPicket(picketIdx) as Number, expData.ab2 as Number, expData.resistanceApparent ).also { pointMap += it to indexMapping[index] } diff --git a/src/main/java/ru/nucodelabs/gem/view/controller/charts/VesCurvesController.kt b/src/main/java/ru/nucodelabs/gem/view/controller/charts/VesCurvesController.kt index b59ebda0..a44825d3 100644 --- a/src/main/java/ru/nucodelabs/gem/view/controller/charts/VesCurvesController.kt +++ b/src/main/java/ru/nucodelabs/gem/view/controller/charts/VesCurvesController.kt @@ -16,8 +16,8 @@ import javafx.scene.chart.XYChart.Series import javafx.scene.control.* import javafx.scene.input.MouseEvent import javafx.scene.input.ScrollEvent +import javafx.scene.layout.VBox import javafx.stage.FileChooser -import javafx.stage.Stage import javafx.util.StringConverter import ru.nucodelabs.gem.app.pref.PNG_FILES_DIR import ru.nucodelabs.gem.config.Name @@ -25,7 +25,6 @@ import ru.nucodelabs.gem.config.Style import ru.nucodelabs.gem.fxmodel.ves.ObservableSection import ru.nucodelabs.gem.view.control.chart.log.LogarithmicAxis import ru.nucodelabs.gem.view.control.chart.log.LogarithmicChartNavigationSupport -import ru.nucodelabs.gem.view.controller.AbstractController import ru.nucodelabs.geo.forward.ForwardSolver import ru.nucodelabs.geo.ves.Picket import ru.nucodelabs.geo.ves.Section @@ -34,6 +33,7 @@ import ru.nucodelabs.geo.ves.calc.graph.* import ru.nucodelabs.geo.ves.calc.resistanceApparentLowerBoundByError import ru.nucodelabs.geo.ves.calc.resistanceApparentUpperBoundByError import ru.nucodelabs.geo.ves.calc.zOfModelLayers +import ru.nucodelabs.kfx.core.AbstractViewController import ru.nucodelabs.kfx.ext.* import ru.nucodelabs.kfx.snapshot.HistoryManager import ru.nucodelabs.util.exp10 @@ -59,7 +59,7 @@ class VesCurvesController @Inject constructor( private val forwardSolver: ForwardSolver, @Named(Name.File.PNG) private val fc: FileChooser, private val prefs: Preferences -) : AbstractController() { +) : AbstractViewController() { private val dataProperty: ObjectProperty>> = initData() @@ -78,9 +78,6 @@ class VesCurvesController @Inject constructor( @FXML private lateinit var yAxis: LogarithmicAxis - override val stage: Stage - get() = lineChart.scene.window as Stage - private val picket get() = picketObservable.get()!! @@ -268,7 +265,7 @@ class VesCurvesController @Inject constructor( val theorCurveSeries = Series() theorCurveSeries.data.addAll( - theoreticalCurve(picket, forwardSolver).map { (x, y) -> Data(x as Number, y as Number) } + theoreticalCurve(picket, picket, forwardSolver).map { (x, y) -> Data(x as Number, y as Number) } ) theorCurveSeries.name = uiProperties["theorCurve"] @@ -392,7 +389,7 @@ class VesCurvesController @Inject constructor( picket.sortedExperimentalData[pointIndex].resistanceApparentUpperBoundByError ) val y = decimalFormat.format(picket.sortedExperimentalData[pointIndex].resistanceApparent) - val theorRes = theoreticalCurve(picket, forwardSolver).getOrNull(pointIndex)?.y + val theorRes = theoreticalCurve(picket, picket, forwardSolver).getOrNull(pointIndex)?.y Tooltip( """ №${pointIndex + 1} diff --git a/src/main/java/ru/nucodelabs/gem/view/controller/main/AddExperimentalDataController.kt b/src/main/java/ru/nucodelabs/gem/view/controller/main/AddExperimentalDataController.kt index dcf336c4..6db1ce2b 100644 --- a/src/main/java/ru/nucodelabs/gem/view/controller/main/AddExperimentalDataController.kt +++ b/src/main/java/ru/nucodelabs/gem/view/controller/main/AddExperimentalDataController.kt @@ -1,29 +1,41 @@ package ru.nucodelabs.gem.view.controller.main -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue import jakarta.inject.Inject -import jakarta.validation.Validator +import javafx.beans.binding.Bindings import javafx.beans.property.IntegerProperty import javafx.fxml.FXML +import javafx.scene.control.Label import javafx.scene.control.TextField +import javafx.scene.control.TextFormatter import javafx.scene.layout.VBox -import javafx.stage.Stage +import javafx.util.StringConverter import ru.nucodelabs.gem.fxmodel.ves.ObservableSection import ru.nucodelabs.gem.view.AlertsFactory -import ru.nucodelabs.gem.view.controller.AbstractController import ru.nucodelabs.geo.ves.ExperimentalData +import ru.nucodelabs.geo.ves.InvalidPropertiesException +import ru.nucodelabs.geo.ves.InvalidPropertyValue import ru.nucodelabs.geo.ves.Section +import ru.nucodelabs.geo.ves.calc.rhoA +import ru.nucodelabs.kfx.core.AbstractViewController import ru.nucodelabs.kfx.snapshot.HistoryManager +import tornadofx.get +import java.net.URL +import java.text.DecimalFormat +import java.text.ParseException +import java.util.* class AddExperimentalDataController @Inject constructor( - private val objectMapper: ObjectMapper, private val alertsFactory: AlertsFactory, private val picketIndex: IntegerProperty, private val observableSection: ObservableSection, private val historyManager: HistoryManager
, - private val validator: Validator -) : AbstractController() { + private val decimalFormat: DecimalFormat, + private val uiProps: ResourceBundle +) : AbstractViewController() { + + @FXML + private lateinit var invalidMessageLabel: Label + @FXML private lateinit var amperageTextField: TextField @@ -42,63 +54,92 @@ class AddExperimentalDataController @Inject constructor( @FXML private lateinit var ab2TextField: TextField - @FXML - private lateinit var root: VBox - - override val stage: Stage? - get() = root.scene.window as Stage? + override fun initialize(location: URL, resources: ResourceBundle) { + super.initialize(location, resources) + val fields = listOf( + ab2TextField to ExperimentalData::validateAb2, + mn2TextField to ExperimentalData::validateMn2, + resAppTextField to ExperimentalData::validateResistApparent, + errResAppTextField to ExperimentalData::validateErrResistApparent, + voltageTextField to ExperimentalData::validateVoltage, + amperageTextField to ExperimentalData::validateAmperage + ) + val converter = object : StringConverter() { + override fun toString(value: Double?): String? = value?.let { + decimalFormat.format(it) + } - fun createJson( - ab2: String, - mn2: String, - amperage: String, - voltage: String, - errorResApp: String, - resApp: String - ): String { - return ("{" + - (if (ab2.isNotBlank()) "\"ab2\": $ab2," else "") + - (if (mn2.isNotBlank()) "\"mn2\": $mn2," else "") + - (if (amperage.isNotBlank()) "\"amperage\": $amperage," else "") + - (if (voltage.isNotBlank()) "\"voltage\": $voltage," else "") + - (if (errorResApp.isNotBlank()) "\"errorResistanceApparent\": $errorResApp," else "") + - (if (resApp.isNotBlank()) "\"resistanceApparent\": $resApp," else "") + - "}").let { - val idx = it.lastIndexOf(",") - it.removeRange(idx, idx + 1) + override fun fromString(value: String?): Double? { + if (value.isNullOrBlank()) return null + return try { + decimalFormat.parse(value).toDouble() + } catch (_: ParseException) { + null + } + } } + fields.forEach { (field, _) -> field.textFormatter = TextFormatter(converter) } + invalidMessageLabel.textProperty().bind( + Bindings.createStringBinding( + { invalidInputMessage(fields) }, + *fields.map { (field, _) -> field.textProperty() }.toTypedArray() + ) + ) + invalidMessageLabel.visibleProperty().bind(invalidMessageLabel.textProperty().map { it.isNotBlank() }) } + private fun invalidInputMessage(fields: List InvalidPropertyValue?>>): String = + fields.map { (field, validate) -> + val value = field.textFormatter.value as Double? + return@map if (value == null) { + // Fill errorResistivityApparent default value if absent + if (field === errResAppTextField) { + @Suppress("unchecked_cast") + (field.textFormatter as TextFormatter).value = ExperimentalData.DEFAULT_ERROR + return@map "" + } + // Fill resistivityApparent formula calculated default value if absent + if (field === resAppTextField) { + val ab2 = ab2TextField.textFormatter.value as Double? + val mn2 = mn2TextField.textFormatter.value as Double? + val amperage = amperageTextField.textFormatter.value as Double? + val voltage = voltageTextField.textFormatter.value as Double? + if (ab2 != null && mn2 != null && amperage != null && voltage != null) { + @Suppress("unchecked_cast") + (field.textFormatter as TextFormatter) + .value = rhoA(ab2, mn2, amperage, voltage) + return@map "" + } + } + "${field.promptText}: ${uiProps["invalidInput"]}" + } else { + validate(value)?.let { (prop, _, _) -> uiProps["invalid.exp.$prop"] } ?: "" + } + }.filter { it.isNotBlank() }.joinToString(separator = "\n") + @FXML private fun add() { val newExpDataItem = try { - objectMapper.readValue( - createJson( - ab2 = ab2TextField.text, - mn2 = mn2TextField.text, - amperage = amperageTextField.text, - voltage = voltageTextField.text, - errorResApp = errResAppTextField.text, - resApp = resAppTextField.text - ) + ExperimentalData( + ab2 = ab2TextField.textFormatter.value as Double, + mn2 = mn2TextField.textFormatter.value as Double, + amperage = amperageTextField.textFormatter.value as Double, + resistanceApparent = resAppTextField.textFormatter.value as Double, + voltage = voltageTextField.textFormatter.value as Double, + errorResistanceApparent = errResAppTextField.textFormatter.value as Double ) - } catch (e: Exception) { + } catch (e: InvalidPropertiesException) { alertsFactory.simpleExceptionAlert(e, stage).show() return } - val violations = validator.validate(newExpDataItem) - if (violations.isEmpty()) { - historyManager.snapshotAfter { - val picket = observableSection.pickets[picketIndex.value] - val expData = picket.sortedExperimentalData - observableSection.pickets[picketIndex.value] = - picket.copy(experimentalData = expData.toMutableList().apply { - add(newExpDataItem) - }) - } - } else { - alertsFactory.violationsAlert(violations, stage).show() + historyManager.snapshotAfter { + val picket = observableSection.pickets[picketIndex.value] + val expData = picket.sortedExperimentalData + observableSection.pickets[picketIndex.value] = + picket.copy(experimentalData = expData.toMutableList().apply { + add(newExpDataItem) + }) } } } \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/gem/view/controller/main/MainViewController.kt b/src/main/java/ru/nucodelabs/gem/view/controller/main/MainViewController.kt index 76a237b7..f1aface1 100644 --- a/src/main/java/ru/nucodelabs/gem/view/controller/main/MainViewController.kt +++ b/src/main/java/ru/nucodelabs/gem/view/controller/main/MainViewController.kt @@ -74,7 +74,8 @@ class MainViewController @Inject constructor( private val preferences: Preferences, private val decimalFormat: DecimalFormat, private val fxPreferences: FXPreferences, - private val inverseSolver: InverseSolver + private val inverseSolver: InverseSolver, + private val ui: ResourceBundle, ) : AbstractController(), FileImporter, FileOpener { private val windowTitle: StringProperty = SimpleStringProperty("GEM") @@ -293,7 +294,7 @@ class MainViewController @Inject constructor( if (newValue != null) { picketOffsetX.text = decimalFormat.format(newValue.offsetX) picketZ.text = decimalFormat.format(newValue.z) - xCoordLbl.text = decimalFormat.format(observableSection.asSection().xOfPicket(picket)) + xCoordLbl.text = decimalFormat.format(observableSection.asSection().xOfPicket(picketIndex)) } } } @@ -677,11 +678,11 @@ class MainViewController @Inject constructor( private fun openAddExpData() { if (addExperimentalData.scene == null) { Stage().apply { - title = "Добавить измерение" + title = ui["addSignal"] initOwner(this@MainViewController.stage) addExperimentalData.stylesheets += Style.COMMON_STYLESHEET scene = Scene(addExperimentalData) - isResizable = false + isResizable = true }.show() } else { (addExperimentalData.scene.window as Stage).show() diff --git a/src/main/java/ru/nucodelabs/gem/view/controller/tables/ExperimentalTableController.kt b/src/main/java/ru/nucodelabs/gem/view/controller/tables/ExperimentalTableController.kt index 30738442..dc280184 100644 --- a/src/main/java/ru/nucodelabs/gem/view/controller/tables/ExperimentalTableController.kt +++ b/src/main/java/ru/nucodelabs/gem/view/controller/tables/ExperimentalTableController.kt @@ -16,6 +16,7 @@ import javafx.scene.input.Clipboard import javafx.scene.input.DataFormat import javafx.scene.input.KeyCode import javafx.scene.input.KeyEvent +import javafx.scene.layout.VBox import javafx.stage.Stage import javafx.util.Callback import javafx.util.StringConverter @@ -24,7 +25,6 @@ import ru.nucodelabs.gem.fxmodel.ves.ObservableExperimentalData import ru.nucodelabs.gem.fxmodel.ves.ObservableSection import ru.nucodelabs.gem.fxmodel.ves.mapper.VesFxModelMapper import ru.nucodelabs.gem.view.AlertsFactory -import ru.nucodelabs.gem.view.controller.AbstractController import ru.nucodelabs.gem.view.controller.FileImporter import ru.nucodelabs.gem.view.controller.main.CalculateErrorScreenController import ru.nucodelabs.gem.view.controller.util.DEFAULT_FONT_SIZE @@ -36,10 +36,8 @@ import ru.nucodelabs.geo.ves.calc.k import ru.nucodelabs.geo.ves.calc.u import ru.nucodelabs.geo.ves.calc.withCalculatedResistanceApparent import ru.nucodelabs.geo.ves.toTabulatedTable -import ru.nucodelabs.kfx.ext.DoubleValidationConverter -import ru.nucodelabs.kfx.ext.bidirectionalNot -import ru.nucodelabs.kfx.ext.decimalFilter -import ru.nucodelabs.kfx.ext.toObservableList +import ru.nucodelabs.kfx.core.AbstractViewController +import ru.nucodelabs.kfx.ext.* import ru.nucodelabs.kfx.snapshot.HistoryManager import ru.nucodelabs.util.TextToTableParser import ru.nucodelabs.util.toDoubleOrNullBy @@ -70,11 +68,12 @@ class ExperimentalTableController @Inject constructor( picketIndexProperty: IntegerProperty, private val historyManager: HistoryManager
, private val alertsFactory: AlertsFactory, - private val doubleStringConverter: StringConverter, + private val converter: StringConverter, private val decimalFormat: DecimalFormat, fileImporterProvider: Provider, - private val mapper: VesFxModelMapper -) : AbstractController(), FileImporter by fileImporterProvider.get() { + private val mapper: VesFxModelMapper, + private val uiProps: ResourceBundle +) : AbstractViewController(), FileImporter by fileImporterProvider.get() { @FXML private lateinit var calculateErrorScreen: Stage @@ -92,35 +91,33 @@ class ExperimentalTableController @Inject constructor( private lateinit var indexCol: TableColumn @FXML - private lateinit var ab2Col: TableColumn + private lateinit var ab2Col: TableColumn @FXML - private lateinit var mn2Col: TableColumn + private lateinit var mn2Col: TableColumn @FXML - private lateinit var resistanceApparentCol: TableColumn + private lateinit var resistanceApparentCol: TableColumn @FXML - private lateinit var errorResistanceCol: TableColumn + private lateinit var errorResistanceCol: TableColumn @FXML - private lateinit var amperageCol: TableColumn + private lateinit var amperageCol: TableColumn @FXML - private lateinit var voltageCol: TableColumn + private lateinit var voltageCol: TableColumn @FXML private lateinit var table: TableView - override val stage: Stage - get() = table.scene.window as Stage - private val picket: Picket get() = picketObservable.get()!! private val picketIndex by picketIndexProperty override fun initialize(location: URL, resources: ResourceBundle) { + table.columnResizePolicy = TableView.CONSTRAINED_RESIZE_POLICY_LAST_COLUMN picketObservable.addListener { _, oldValue: Picket?, newValue: Picket? -> if (newValue != null) { if (oldValue != null @@ -189,24 +186,39 @@ class ExperimentalTableController @Inject constructor( isHiddenCol.cellValueFactory = Callback { features -> features.value.hiddenProperty().bidirectionalNot() } isHiddenCol.cellFactory = CheckBoxTableCell.forTableColumn(isHiddenCol) - ab2Col.cellValueFactory = Callback { features -> features.value.ab2Property().asObject() } - mn2Col.cellValueFactory = Callback { features -> features.value.mn2Property().asObject() } - resistanceApparentCol.cellValueFactory = - Callback { features -> features.value.resistanceApparentProperty().asObject() } - errorResistanceCol.cellValueFactory = - Callback { features -> features.value.errorResistanceApparentProperty().asObject() } - amperageCol.cellValueFactory = Callback { features -> features.value.amperageProperty().asObject() } - voltageCol.cellValueFactory = Callback { features -> features.value.voltageProperty().asObject() } + ab2Col.cellValueFactory = Callback { features -> features.value.ab2Property() } + mn2Col.cellValueFactory = Callback { features -> features.value.mn2Property() } + resistanceApparentCol.cellValueFactory = Callback { features -> features.value.resistanceApparentProperty() } + errorResistanceCol.cellValueFactory = Callback { features -> features.value.errorResistanceApparentProperty() } + amperageCol.cellValueFactory = Callback { features -> features.value.amperageProperty() } + voltageCol.cellValueFactory = Callback { features -> features.value.voltageProperty() } val editableColumns = listOf( - ab2Col, - mn2Col, - resistanceApparentCol, - errorResistanceCol, - amperageCol, - voltageCol + ab2Col to ExperimentalData::validateAb2, + mn2Col to ExperimentalData::validateMn2, + resistanceApparentCol to ExperimentalData::validateResistApparent, + errorResistanceCol to ExperimentalData::validateErrResistApparent, + amperageCol to ExperimentalData::validateAmperage, + voltageCol to ExperimentalData::validateVoltage, ) - editableColumns.forEach { it.cellFactory = TextFieldTableCell.forTableColumn(doubleStringConverter) } + + editableColumns.forEach { (col, validate) -> + col.cellFactory = Callback { _ -> TextFieldTableCell(converter) } + + val onEditCommitHandler = col.onEditCommit + col.onEditCommit = EventHandler { event -> + if (event.newValue == null) { + event.consume() + return@EventHandler + } + validate(event.newValue.toDouble())?.let { (prop, _) -> + alertsFactory.invalidInputAlert(uiProps["invalid.exp.$prop"]).show() + event.consume() + return@EventHandler + } + onEditCommitHandler.handle(event) + } + } } private fun setupRowFactory() { @@ -297,70 +309,12 @@ class ExperimentalTableController @Inject constructor( private fun listenToItemsProperties(items: List) { items.forEach { expData -> - expData.ab2Property().addListener { _, oldAb2, newAb2 -> - val violations = validator.validateValue(ExperimentalData::class.java, "ab2", newAb2) - if (violations.isEmpty()) { - commitChanges() - } else { - expData.ab2 = oldAb2.toDouble() - alertsFactory.violationsAlert(violations, stage).show() - } - } - expData.mn2Property().addListener { _, oldMn2, newMn2 -> - val violations = validator.validateValue(ExperimentalData::class.java, "mn2", newMn2) - if (violations.isEmpty()) { - commitChanges() - } else { - expData.mn2 = oldMn2.toDouble() - alertsFactory.violationsAlert(violations, stage).show() - } - } - expData.errorResistanceApparentProperty().addListener { _, oldErr, newErr -> - val violations = validator.validateValue( - ExperimentalData::class.java, - "errorResistanceApparent", - newErr - ) - if (violations.isEmpty() - ) { - commitChanges() - } else { - expData.errorResistanceApparent = oldErr.toDouble() - alertsFactory.violationsAlert(violations, stage).show() - } - } - expData.amperageProperty().addListener { _, oldAmp, newAmp -> - val violations = validator.validateValue(ExperimentalData::class.java, "amperage", newAmp) - if (violations.isEmpty()) { - commitChanges() - } else { - expData.amperage = oldAmp.toDouble() - alertsFactory.violationsAlert(violations, stage).show() - } - } - expData.voltageProperty().addListener { _, oldVolt, newVolt -> - val violations = validator.validateValue(ExperimentalData::class.java, "voltage", newVolt) - if (violations.isEmpty()) { - commitChanges() - } else { - expData.voltage = oldVolt.toDouble() - alertsFactory.violationsAlert(violations, stage).show() - } - } - expData.resistanceApparentProperty().addListener { _, oldRes, newRes -> - val violations = validator.validateValue( - ExperimentalData::class.java, - "resistanceApparent", - newRes - ) - if (violations.isEmpty() - ) { - commitChanges() - } else { - expData.resistanceApparent = oldRes.toDouble() - alertsFactory.violationsAlert(violations, stage).show() - } - } + expData.ab2Property().addListener { _, oldAb2, newAb2 -> commitChanges() } + expData.mn2Property().addListener { _, oldMn2, newMn2 -> commitChanges() } + expData.errorResistanceApparentProperty().addListener { _, oldErr, newErr -> commitChanges() } + expData.amperageProperty().addListener { _, oldAmp, newAmp -> commitChanges() } + expData.voltageProperty().addListener { _, oldVolt, newVolt -> commitChanges() } + expData.resistanceApparentProperty().addListener { _, oldRes, newRes -> commitChanges() } expData.hiddenProperty().addListener { _, _, isHidden -> if (table.selectionModel.selectedItems.isEmpty()) { toggleSingleHidden(expData, isHidden) @@ -398,7 +352,7 @@ class ExperimentalTableController @Inject constructor( if (calculateErrorScreen.owner == null) { calculateErrorScreen.initOwner(stage) } - calculateErrorScreen.icons.setAll(stage.icons) + calculateErrorScreen.icons.setAll(stage?.icons) } private fun setIsHiddenOnSelected(isHidden: Boolean) { @@ -457,7 +411,7 @@ class ExperimentalTableController @Inject constructor( val text = Clipboard.getSystemClipboard().string val parser = if (text != null) TextToTableParser(text) else return try { - val a = "abcdefghijklmnopqrstuvwxyz".uppercase().toCharArray() + val a = "abcdefghijksssddddmnopqrstuvwxyz".uppercase().toCharArray() val parsedTable = parser.parsedTable.filter { row -> row.none { it == null } } fun String?.process(expected: String, row: Int, col: Int) = diff --git a/src/main/java/ru/nucodelabs/gem/view/controller/tables/ModelTableController.kt b/src/main/java/ru/nucodelabs/gem/view/controller/tables/ModelTableController.kt index 3b38536d..e243ae89 100644 --- a/src/main/java/ru/nucodelabs/gem/view/controller/tables/ModelTableController.kt +++ b/src/main/java/ru/nucodelabs/gem/view/controller/tables/ModelTableController.kt @@ -2,7 +2,6 @@ package ru.nucodelabs.gem.view.controller.tables import jakarta.inject.Inject import jakarta.inject.Provider -import jakarta.validation.Validator import javafx.beans.binding.Bindings.createBooleanBinding import javafx.beans.binding.Bindings.createStringBinding import javafx.beans.property.IntegerProperty @@ -15,7 +14,6 @@ import javafx.scene.control.* import javafx.scene.control.cell.TextFieldTableCell import javafx.scene.input.* import javafx.scene.layout.VBox -import javafx.stage.Stage import javafx.util.Callback import javafx.util.StringConverter import ru.nucodelabs.gem.fxmodel.ves.ObservableModelLayer @@ -23,7 +21,6 @@ import ru.nucodelabs.gem.fxmodel.ves.ObservableSection import ru.nucodelabs.gem.fxmodel.ves.app.VesFxAppModel import ru.nucodelabs.gem.fxmodel.ves.mapper.VesFxModelMapper import ru.nucodelabs.gem.view.AlertsFactory -import ru.nucodelabs.gem.view.controller.AbstractController import ru.nucodelabs.gem.view.controller.FileImporter import ru.nucodelabs.gem.view.controller.main.InitialModelConfigurationViewController import ru.nucodelabs.gem.view.controller.util.DEFAULT_FONT_SIZE @@ -35,6 +32,8 @@ import ru.nucodelabs.geo.ves.calc.divide import ru.nucodelabs.geo.ves.calc.join import ru.nucodelabs.geo.ves.calc.zOfModelLayers import ru.nucodelabs.geo.ves.toTabulatedTable +import ru.nucodelabs.kfx.core.AbstractViewController +import ru.nucodelabs.kfx.ext.get import ru.nucodelabs.kfx.ext.toObservableList import ru.nucodelabs.kfx.snapshot.HistoryManager import ru.nucodelabs.util.Err @@ -64,13 +63,13 @@ class ModelTableController @Inject constructor( private val alertsFactory: AlertsFactory, private val observableSection: ObservableSection, private val picketIndexProperty: IntegerProperty, - private val validator: Validator, private val historyManager: HistoryManager
, - private val doubleStringConverter: StringConverter, + private val converter: StringConverter, private val decimalFormat: DecimalFormat, private val mapper: VesFxModelMapper, - private val appModel: VesFxAppModel -) : AbstractController(), FileImporter by fileImporterProvider.get() { + private val appModel: VesFxAppModel, + private val uiProps: ResourceBundle +) : AbstractViewController(), FileImporter by fileImporterProvider.get() { @FXML private lateinit var copyFromRightBtn: Button @@ -79,16 +78,16 @@ class ModelTableController @Inject constructor( private lateinit var copyFromLeftBtn: Button @FXML - private lateinit var zCol: TableColumn + private lateinit var zCol: TableColumn @FXML private lateinit var indexCol: TableColumn @FXML - private lateinit var powerCol: TableColumn + private lateinit var powerCol: TableColumn @FXML - private lateinit var resistanceCol: TableColumn + private lateinit var resistanceCol: TableColumn @FXML private lateinit var table: TableView @@ -99,16 +98,13 @@ class ModelTableController @Inject constructor( @FXML private lateinit var initialModelConfigurationViewController: InitialModelConfigurationViewController - override val stage: Stage? - get() = table.scene.window as Stage? - - private val picket: Picket get() = picketObservable.get()!! private val picketIndex by picketIndexProperty override fun initialize(location: URL, resources: ResourceBundle) { + table.columnResizePolicy = TableView.CONSTRAINED_RESIZE_POLICY_LAST_COLUMN picketObservable.addListener { _, oldValue, newValue -> newValue?.let { if (oldValue != null @@ -293,10 +289,10 @@ class ModelTableController @Inject constructor( private fun setupCellFactories() { indexCol.cellFactory = indexCellFactory() - powerCol.cellValueFactory = Callback { features -> features.value.powerProperty().asObject() } - resistanceCol.cellValueFactory = Callback { features -> features.value.resistanceProperty().asObject() } + powerCol.cellValueFactory = Callback { features -> features.value.powerProperty() } + resistanceCol.cellValueFactory = Callback { features -> features.value.resistanceProperty() } zCol.cellFactory = Callback { - TableCell().apply { + TableCell().apply { textProperty().bind( createStringBinding( { @@ -316,34 +312,41 @@ class ModelTableController @Inject constructor( } val editableColumns = listOf( - powerCol, - resistanceCol + powerCol to ModelLayer::validatePower, + resistanceCol to ModelLayer::validateResistivity ) - editableColumns.forEach { - it.cellFactory = Callback { col -> - TextFieldTableCell.forTableColumn(doubleStringConverter).call(col).apply { + editableColumns.forEach { (col, validate) -> + col.cellFactory = Callback { _ -> + TextFieldTableCell(converter).apply { when (col) { - powerCol -> indexProperty().addListener { _, _, _ -> - if (index >= 0 && index <= picket.modelData.lastIndex) { - style = if (picket.modelData[index].isFixedPower) { - STYLE_FOR_FIXED - } else { - "" - } - } - } + powerCol -> tableRowProperty() + .flatMap { it.itemProperty() } + .flatMap { it.fixedPowerProperty() } + .addListener { _, _, isFixed -> style = if (isFixed ?: false) STYLE_FOR_FIXED else "" } + + + resistanceCol -> tableRowProperty() + .flatMap { it.itemProperty() } + .flatMap { it.fixedResistanceProperty() } + .addListener { _, _, isFixed -> style = if (isFixed ?: false) STYLE_FOR_FIXED else "" } - resistanceCol -> indexProperty().addListener { _, _, _ -> - if (index >= 0 && index <= picket.modelData.lastIndex) { - style = if (picket.modelData[index].isFixedResistance) { - STYLE_FOR_FIXED - } else { - "" - } - } - } } + + } + } + + val onEditCommitHandler = col.onEditCommit + col.onEditCommit = EventHandler { event -> + if (event.newValue == null) { + event.consume() + return@EventHandler + } + validate(event.newValue.toDouble())?.let { (prop, _) -> + alertsFactory.invalidInputAlert(uiProps["invalid.model.$prop"]).show() + event.consume() + return@EventHandler } + onEditCommitHandler.handle(event) } } @@ -393,44 +396,19 @@ class ModelTableController @Inject constructor( private fun listenToItemsProperties(items: List) { items.forEach { layer -> - layer.powerProperty().addListener { _, oldPow, newPow -> - val violations = validator.validateValue(ModelLayer::class.java, "power", newPow) - if (violations.isEmpty()) { - commitChanges() - update() - } else { - layer.power = oldPow.toDouble() - alertsFactory.violationsAlert(violations, stage).show() - } - } - layer.resistanceProperty().addListener { _, oldRes, newRes -> - val violations = validator.validateValue(ModelLayer::class.java, "resistance", newRes) - if (violations.isEmpty()) { - commitChanges() - } else { - layer.resistance = oldRes.toDouble() - alertsFactory.violationsAlert(violations, stage).show() - } - } + layer.powerProperty().addListener { _, oldPow, newPow -> commitChanges() } + layer.resistanceProperty().addListener { _, oldRes, newRes -> commitChanges() } layer.fixedPowerProperty().addListener { _, _, _ -> commitChanges() } layer.fixedResistanceProperty().addListener { _, _, _ -> commitChanges() } } } private fun commitChanges() { - val modelDataInTable = table.items.map { mapper.toModel(it) } - if (modelDataInTable != picket.modelData) { - val update = picket.copied(modelData = modelDataInTable) - when (update) { - is Err -> { - alertsFactory.simpleAlert(text = update.error.joinToString(separator = "\n")).show() - } - - is Ok -> { - historyManager.snapshotAfter { - observableSection.pickets[picketIndex] = update.value - } - } + val mappedModel = table.items.map { mapper.toModel(it) } + if (mappedModel != picket.modelData) { + val update = picket.copy(modelData = mappedModel) + historyManager.snapshotAfter { + observableSection.pickets[picketIndex] = update } } } @@ -500,11 +478,11 @@ class ModelTableController @Inject constructor( ?.toDoubleOrNullBy(decimalFormat) ?: throw IllegalArgumentException("${a[col]}${row + 1} - Ожидалось $expected, было $this") - val pastedItems: List = when (parser.columnsCount) { + val pastedItems = when (parser.columnsCount) { 2 -> parsedTable.mapIndexed { rowIdx, row -> val pow = row[0].process("H", rowIdx, 0) val res = row[1].process("ρ", rowIdx, 1) - ModelLayer( + ModelLayer.new( resistance = res, power = pow ) @@ -522,14 +500,18 @@ class ModelTableController @Inject constructor( ) } } + val mapped = ArrayList() for (item in pastedItems) { - val violations = validator.validate(item) - if (violations.isNotEmpty()) { - alertsFactory.violationsAlert(violations, stage).show() - return + when (item) { + is Err -> { + alertsFactory.simpleAlert(text = item.error.joinToString()) + return + } + + is Ok -> mapped.add(mapper.toObservable(item.value)) } } - table.items += pastedItems.map { mapper.toObservable(it) } + table.items += mapped } catch (e: Exception) { alertsFactory.simpleExceptionAlert(e, stage).show() } diff --git a/src/main/java/ru/nucodelabs/geo/anisotropy/Signal.kt b/src/main/java/ru/nucodelabs/geo/anisotropy/Signal.kt index 5b1b851f..345baf39 100644 --- a/src/main/java/ru/nucodelabs/geo/anisotropy/Signal.kt +++ b/src/main/java/ru/nucodelabs/geo/anisotropy/Signal.kt @@ -26,22 +26,27 @@ data class Signal( @field:DecimalMin(MIN_RESISTANCE.toString()) var resistanceApparent: Double = rhoA(ab2, mn2, amperage, voltage), @field:Min(0) @field:Max(100) var errorResistanceApparent: Double = DEFAULT_ERROR, val isHidden: Boolean = false -) +) { + companion object Factory { + @Suppress("unused") + @JvmStatic + fun withCalculatedVoltage( + ab2: Double, + mn2: Double, + resistanceApparent: Double, + amperage: Double = DEFAULT_AMPERAGE, + voltage: Double = u(resistanceApparent, amperage, k(ab2, mn2)), + errorResistanceApparent: Double = DEFAULT_ERROR, + isHidden: Boolean = false + ) = Signal( + ab2 = ab2, + mn2 = mn2, + amperage = amperage, + voltage = voltage, + resistanceApparent = resistanceApparent, + errorResistanceApparent = errorResistanceApparent, + isHidden = isHidden + ) + } +} -fun signalWithOnlyRhoA( - ab2: Double, - mn2: Double, - resistanceApparent: Double, - amperage: Double = DEFAULT_AMPERAGE, - voltage: Double = u(resistanceApparent, amperage, k(ab2, mn2)), - errorResistanceApparent: Double = DEFAULT_ERROR, - isHidden: Boolean = false -) = Signal( - ab2 = ab2, - mn2 = mn2, - amperage = amperage, - voltage = voltage, - resistanceApparent = resistanceApparent, - errorResistanceApparent = errorResistanceApparent, - isHidden = isHidden -) diff --git a/src/main/java/ru/nucodelabs/geo/ves/ExperimentalData.kt b/src/main/java/ru/nucodelabs/geo/ves/ExperimentalData.kt index 33fe236f..54f34c8e 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/ExperimentalData.kt +++ b/src/main/java/ru/nucodelabs/geo/ves/ExperimentalData.kt @@ -1,10 +1,30 @@ package ru.nucodelabs.geo.ves -import jakarta.validation.constraints.DecimalMin -import jakarta.validation.constraints.Max -import jakarta.validation.constraints.Min -import jakarta.validation.constraints.Positive import ru.nucodelabs.geo.ves.calc.rhoA +import ru.nucodelabs.util.Result +import ru.nucodelabs.util.toErrorResult +import ru.nucodelabs.util.toOkResult +import ru.nucodelabs.util.validate + +interface ReadOnlyExperimentalSignal { + val ab2: Double + val mn2: Double + val amperage: Double + val voltage: Double + val resistanceApparent: Double + val errorResistanceApparent: Double + val isHidden: Boolean +} + +interface MutableExperimentalSignal : ReadOnlyExperimentalSignal { + override var ab2: Double + override var mn2: Double + override var amperage: Double + override var voltage: Double + override var resistanceApparent: Double + override var errorResistanceApparent: Double + override var isHidden: Boolean +} /** * Экспериментальное измерение @@ -17,15 +37,105 @@ import ru.nucodelabs.geo.ves.calc.rhoA * @property isHidden Отключена для интерпретации */ data class ExperimentalData( - @field:Positive val ab2: Double, - @field:Positive val mn2: Double, - @field:Min(0) val amperage: Double, - @field:Min(0) val voltage: Double, - @field:DecimalMin("0.1") val resistanceApparent: Double = rhoA(ab2, mn2, amperage, voltage), - @field:Min(0) @field:Max(100) val errorResistanceApparent: Double = DEFAULT_ERROR, - val isHidden: Boolean = false -) { - companion object Defaults { + override val ab2: Double, + override val mn2: Double, + override val amperage: Double, + override val voltage: Double, + override val resistanceApparent: Double = rhoA(ab2, mn2, amperage, voltage), + override val errorResistanceApparent: Double = DEFAULT_ERROR, + override val isHidden: Boolean = false +) : ReadOnlyExperimentalSignal { + + init { + val errors = listOfNotNull( + validateAb2(ab2), + validateMn2(mn2), + validateAmperage(amperage), + validateVoltage(voltage), + validateResistApparent(resistanceApparent), + validateErrResistApparent(errorResistanceApparent), + ) + if (errors.isNotEmpty()) throw InvalidPropertiesException(errors) + } + + companion object Meta { const val DEFAULT_ERROR = 5.0 + + const val MIN_DIST = .0 + const val MIN_AMP = .0 + const val MIN_VOLT = .0 + const val MIN_RESIST_APP = 0.1 + const val MIN_ERROR_RESIST_APP = .0 + const val MAX_ERROR_RESIST_APP = 100.0 + + const val AB2 = "ab2" + fun validateAb2(ab2: Double) = validate(isValidDistance(ab2)) { + InvalidPropertyValue(AB2, "AB/2 must be >= $MIN_DIST", ab2) + } + + const val MN2 = "mn2" + fun validateMn2(mn2: Double) = validate(isValidDistance(mn2)) { + InvalidPropertyValue(MN2, "MN/2 must be >= $MIN_DIST", mn2) + } + + fun isValidDistance(dist: Double) = dist >= MIN_DIST + + const val AMPERAGE = "amperage" + fun validateAmperage(amperage: Double) = validate(isValidAmperage(amperage)) { + InvalidPropertyValue(AMPERAGE, "Amperage must be >= $MIN_AMP", amperage) + } + + fun isValidAmperage(amperage: Double) = amperage >= MIN_AMP + + const val VOLTAGE = "voltage" + fun validateVoltage(voltage: Double) = validate(isValidVoltage(voltage)) { + InvalidPropertyValue(VOLTAGE, "Voltage must be >= $MIN_VOLT", voltage) + } + + fun isValidVoltage(voltage: Double) = voltage >= MIN_VOLT + + const val RESIST_APP = "resistivityApparent" + fun validateResistApparent(resistApparent: Double) = validate(isValidResistApparent(resistApparent)) { + InvalidPropertyValue(RESIST_APP, "Resistivity Apparent must be >= $MIN_RESIST_APP", resistApparent) + } + + fun isValidResistApparent(resistApparent: Double) = resistApparent >= MIN_RESIST_APP + + const val ERR_RESIST_APP = "errorResistivityApparent" + fun validateErrResistApparent(errResistApparent: Double) = + validate(isValidErrResistApparent(errResistApparent)) { + InvalidPropertyValue( + ERR_RESIST_APP, + "Error for Resist Apparent must be in range $MIN_ERROR_RESIST_APP - $MAX_ERROR_RESIST_APP %", + errResistApparent + ) + } + + fun isValidErrResistApparent(errResistApparent: Double) = + errResistApparent in MIN_ERROR_RESIST_APP..MAX_ERROR_RESIST_APP + + fun new( + ab2: Double, + mn2: Double, + amperage: Double, + voltage: Double, + resistanceApparent: Double = rhoA(ab2, mn2, amperage, voltage), + errorResistanceApparent: Double = DEFAULT_ERROR, + isHidden: Boolean = false + ): Result> { + return try { + ExperimentalData( + ab2, + mn2, + amperage, + voltage, + resistanceApparent, + errorResistanceApparent, + isHidden + ).toOkResult() + } catch (e: InvalidPropertiesException) { + return e.errors.toErrorResult() + } + } } } diff --git a/src/main/java/ru/nucodelabs/geo/ves/ExperimentalDataSet.kt b/src/main/java/ru/nucodelabs/geo/ves/ExperimentalDataSet.kt new file mode 100644 index 00000000..8ebfc9f4 --- /dev/null +++ b/src/main/java/ru/nucodelabs/geo/ves/ExperimentalDataSet.kt @@ -0,0 +1,57 @@ +package ru.nucodelabs.geo.ves + +import ru.nucodelabs.geo.ves.calc.orderByDistances + + +interface ExperimentalDataSet { + val sortedExperimentalData: List + val effectiveExperimentalData: List + val offsetX: Double +} + +interface MutableExperimentalDataSet : ExperimentalDataSet { + fun addSignal(signal: E) + fun addSignals(signals: Iterable) + fun removeSignal(signal: E) + fun removeSignals(signals: Iterable) + fun edit(index: Int, mutate: MutableExperimentalSignal.() -> Unit) +} + +fun toSortedExperimentalData(experimentalData: List): List { + val acc = mutableListOf() + + // Группируем по AB + val dupGroups = experimentalData.groupBy { it.ab2 } + for ((_, group) in dupGroups) { + // Если больше одного не отключенного дубликата в группе + acc += if (group.filter { !it.isHidden }.size > 1) { + val sortedGroup = group.sortedWith(orderByDistances()) + List(sortedGroup.size) { idx -> + // Отключаем все кроме последнего (с наиб. MN) + if (idx < sortedGroup.lastIndex) { + sortedGroup[idx].copy(isHidden = true) + } else { + sortedGroup[idx].copy(isHidden = false) + } + } + } else { + group + } + } + + return acc.sortedWith(orderByDistances()) +} + +fun hideExtraSignalsInPlace(sortedExperimentalData: List) { + val groupsByAb = sortedExperimentalData.groupBy { it.ab2 } + + for ((_, group) in groupsByAb) { + if (group.filter { !it.isHidden }.size <= 1) { + continue + } else { + group.sortedWith(orderByDistances()).forEachIndexed { idx, item -> + item.isHidden = idx != group.lastIndex + } + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/geo/ves/InvalidPropertiesException.kt b/src/main/java/ru/nucodelabs/geo/ves/InvalidPropertiesException.kt new file mode 100644 index 00000000..62b5e5ad --- /dev/null +++ b/src/main/java/ru/nucodelabs/geo/ves/InvalidPropertiesException.kt @@ -0,0 +1,11 @@ +package ru.nucodelabs.geo.ves + +class InvalidPropertiesException( + val errors: List +) : Exception(errors.joinToString()) + +data class InvalidPropertyValue( + val property: String, + val message: String, + val value: Any? +) \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/geo/ves/ModelDataSet.kt b/src/main/java/ru/nucodelabs/geo/ves/ModelDataSet.kt new file mode 100644 index 00000000..d615c093 --- /dev/null +++ b/src/main/java/ru/nucodelabs/geo/ves/ModelDataSet.kt @@ -0,0 +1,6 @@ +package ru.nucodelabs.geo.ves + +interface ModelDataSet { + val modelData: List + val modelZ: Double +} diff --git a/src/main/java/ru/nucodelabs/geo/ves/ModelLayer.kt b/src/main/java/ru/nucodelabs/geo/ves/ModelLayer.kt index 5d58c31a..193ab35e 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/ModelLayer.kt +++ b/src/main/java/ru/nucodelabs/geo/ves/ModelLayer.kt @@ -1,7 +1,23 @@ package ru.nucodelabs.geo.ves -import jakarta.validation.constraints.DecimalMin -import jakarta.validation.constraints.Min +import ru.nucodelabs.util.Result +import ru.nucodelabs.util.toErrorResult +import ru.nucodelabs.util.toOkResult +import ru.nucodelabs.util.validate + +interface ReadOnlyModelLayer { + val power: Double + val resistance: Double + val isFixedPower: Boolean + val isFixedResistance: Boolean +} + +interface MutableModelLayer : ReadOnlyModelLayer { + override var power: Double + override var resistance: Double + override var isFixedPower: Boolean + override var isFixedResistance: Boolean +} /** * Слой модели @@ -11,8 +27,60 @@ import jakarta.validation.constraints.Min * @property isFixedResistance Значение зафиксировано для обратной задачи */ data class ModelLayer( - @field:Min(0) val power: Double, - @field:DecimalMin("0.1") val resistance: Double, - val isFixedPower: Boolean = false, - val isFixedResistance: Boolean = false -) + override val power: Double, + override val resistance: Double, + override val isFixedPower: Boolean = false, + override val isFixedResistance: Boolean = false +) : ReadOnlyModelLayer { + init { + val errors = listOfNotNull( + validatePower(power), + validateResistivity(resistance) + ) + if (errors.isNotEmpty()) throw InvalidPropertiesException(errors) + } + + companion object Meta { + const val MIN_POWER = .0 + const val MIN_RESIST = 0.1 + + fun validatePower(power: Double) = validate(isValidPower(power)) { + InvalidPropertyValue("power", "Power=$power must be >= $MIN_POWER", power) + } + + fun isValidPower(power: Double): Boolean = power >= MIN_POWER + + fun validateResistivity(resist: Double) = validate(isValidResist(resist)) { + InvalidPropertyValue("resistivity", "Resistivity=$resist must be >= $MIN_RESIST", resist) + } + + fun isValidResist(resist: Double) = resist >= MIN_RESIST + + fun from( + modelLayer: ReadOnlyModelLayer + ) = new( + power = modelLayer.power, + resistance = modelLayer.resistance, + isFixedPower = modelLayer.isFixedPower, + isFixedResistance = modelLayer.isFixedResistance + ) + + fun new( + power: Double, + resistance: Double, + isFixedPower: Boolean = false, + isFixedResistance: Boolean = false + ): Result> { + return try { + ModelLayer( + power, + resistance, + isFixedPower, + isFixedResistance + ).toOkResult() + } catch (e: InvalidPropertiesException) { + e.errors.toErrorResult() + } + } + } +} diff --git a/src/main/java/ru/nucodelabs/geo/ves/Picket.kt b/src/main/java/ru/nucodelabs/geo/ves/Picket.kt index 26213772..3a8f4e96 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/Picket.kt +++ b/src/main/java/ru/nucodelabs/geo/ves/Picket.kt @@ -2,10 +2,6 @@ package ru.nucodelabs.geo.ves import com.fasterxml.jackson.annotation.JsonGetter import com.fasterxml.jackson.annotation.JsonIgnore -import jakarta.validation.Valid -import jakarta.validation.constraints.DecimalMin -import jakarta.validation.constraints.Size -import ru.nucodelabs.geo.ves.calc.ExperimentalDataSort import ru.nucodelabs.util.* import java.util.* @@ -20,38 +16,66 @@ import java.util.* */ class Picket private constructor( val name: String, - experimentalData: List<@Valid ExperimentalData>, - modelData: List<@Valid ModelLayer>, - @get:DecimalMin(MIN_OFFSET_X.toString()) val offsetX: Double, + experimentalData: List, + override val modelData: List, + override val offsetX: Double, val z: Double, val comment: String, skipExperimentalDataProcessing: Boolean = false, - skipModelDataProcessing: Boolean = false, -) { - - @JsonIgnore - val id: UUID = UUID.randomUUID() +) : ExperimentalDataSet, ModelDataSet { + constructor( + name: String = DEFAULT_NAME, + experimentalData: List = emptyList(), + modelData: List = emptyList(), + offsetX: Double = DEFAULT_X_OFFSET, + z: Double = DEFAULT_Z, + comment: String = DEFAULT_COMMENT + ) : this( + name, + experimentalData, + modelData, + offsetX, + z, + comment, + skipExperimentalDataProcessing = false, + ) - @get:Size(max = MAX_MODEL_DATA_SIZE) - val modelData by lazy { - if (skipModelDataProcessing) return@lazy modelData - preprocessModelData(modelData) + init { + val errors = listOfNotNull( + validate(isValidModelDataSize(modelData.size)) { + InvalidPropertyValue( + "modelData.size", + "Model layers count must be ≤ $MAX_MODEL_DATA_SIZE layers", + modelData.size + ) + }, + validate(isValidOffsetX(offsetX)) { + InvalidPropertyValue("offsetX", "X-Offset must be >= $MIN_OFFSET_X", offsetX) + }, + ) + if (errors.isNotEmpty()) throw InvalidPropertiesException(errors) } + + override val modelZ: Double = z + + @JsonIgnore + val id: UUID = UUID.randomUUID() // todo remove, make name unique + /** * Полевые(экспериментальные) данные, отсортированы по AB/2 затем по MN/2 */ @get:JsonGetter("experimentalData") - val sortedExperimentalData: List by lazy { + override val sortedExperimentalData: List by lazy { if (skipExperimentalDataProcessing) return@lazy experimentalData - preprocessExperimentalData(experimentalData) + toSortedExperimentalData(experimentalData) } /** * Без отключенных и если одинаковые AB/2, то с наибольшим MN/2 */ @get:JsonIgnore - val effectiveExperimentalData: List by lazy { + override val effectiveExperimentalData: List by lazy { this.sortedExperimentalData.filter { !it.isHidden } } @@ -89,14 +113,15 @@ class Picket private constructor( offsetX: Double = this.offsetX, z: Double = this.z, comment: String = this.comment - ): Picket = copied( + ): Picket = Picket( name, experimentalData, modelData, offsetX, z, - comment - ).okOrThrow { IllegalArgumentException(it.joinToString()) } + comment, + skipExperimentalDataProcessing = this.sortedExperimentalData == experimentalData, + ) fun copied( name: String = this.name, @@ -105,57 +130,21 @@ class Picket private constructor( offsetX: Double = this.offsetX, z: Double = this.z, comment: String = this.comment - ): Result> = internalNew( - name, - experimentalData, - modelData, - offsetX, - z, - comment, - skipExperimentalDataProcessing = this.sortedExperimentalData === experimentalData, - skipModelDataProcessing = this.modelData === modelData - ) - - private fun preprocessExperimentalData(experimentalData: List): List { - val acc = mutableListOf() - - // Группируем по AB - val dupGroups = experimentalData.groupBy { it.ab2 } - for ((_, group) in dupGroups) { - // Если больше одного не отключенного дубликата в группе - acc += if (group.filter { !it.isHidden }.size > 1) { - val sortedGroup = group.sortedWith(ExperimentalDataSort.orderByDistances) - List(sortedGroup.size) { - // Отключаем все кроме последнего (с наиб. MN) - if (it < sortedGroup.lastIndex) { - sortedGroup[it].copy(isHidden = true) - } else { - sortedGroup[it].copy(isHidden = false) - } - } - } else { - group - } - } - - return acc.sortedWith(ExperimentalDataSort.orderByDistances) - } - - fun preprocessModelData(raw: List): List { - return raw.toMutableList().also { - if (it.isNotEmpty()) { - for (i in it.indices) { - if (it[i].power.isNaN()) { - it[i] = it[i].copy(power = 0.0) - } - } - it[it.lastIndex] = it.last().copy(power = Double.NaN) - } - } + ): Result> = try { + copy( + name, + experimentalData, + modelData, + offsetX, + z, + comment, + ).toOkResult() + } catch (e: InvalidPropertiesException) { + e.errors.toErrorResult() } - companion object { - const val DEFAULT_OFFSET_X = 100.0 + companion object Meta { + const val DEFAULT_X_OFFSET = 100.0 const val DEFAULT_NAME = "Пикет" const val DEFAULT_Z = 0.0 const val DEFAULT_COMMENT = "" @@ -163,74 +152,30 @@ class Picket private constructor( const val MAX_MODEL_DATA_SIZE = 40 const val MIN_OFFSET_X = 0.0 - /** - * Validate and create - */ - fun new( - name: String = DEFAULT_NAME, - experimentalData: List = emptyList(), - modelData: List = emptyList(), - offsetX: Double = DEFAULT_OFFSET_X, - z: Double = DEFAULT_Z, - comment: String = DEFAULT_COMMENT - ): Result> { - return internalNew( - name, - experimentalData, - modelData, - offsetX, - z, - comment - ) - } + fun isValidOffsetX(offsetX: Double): Boolean = offsetX >= MIN_OFFSET_X - private fun internalNew( - name: String, - experimentalData: List, - modelData: List, - offsetX: Double, - z: Double, - comment: String, - skipExperimentalDataProcessing: Boolean = false, - skipModelDataProcessing: Boolean = false - ): Result> { - val errors = ArrayList() - validate(modelData.size <= MAX_MODEL_DATA_SIZE) { - "Model layers count must be ≤ $MAX_MODEL_DATA_SIZE layers" - }?.let { errors += it } - validate(offsetX >= MIN_OFFSET_X) { "X-Offset must be zero or positive" }?.let { errors += it } - if (errors.isNotEmpty()) return Err(errors) - return Picket( - name, - experimentalData, - modelData, - offsetX, - z, - comment, - skipExperimentalDataProcessing, - skipModelDataProcessing - ).toOkResult() - } + fun isValidModelDataSize(modelDataSize: Int): Boolean = modelDataSize <= MAX_MODEL_DATA_SIZE - /** - * Classic style initialization. Throws on invalid input. - * - * Backwards compatibility. - */ - operator fun invoke( + fun new( name: String = DEFAULT_NAME, experimentalData: List = emptyList(), modelData: List = emptyList(), - offsetX: Double = DEFAULT_OFFSET_X, + offsetX: Double = DEFAULT_X_OFFSET, z: Double = DEFAULT_Z, comment: String = DEFAULT_COMMENT - ): Picket = new( - name, - experimentalData, - modelData, - offsetX, - z, - comment - ).okOrThrow { IllegalArgumentException(it.joinToString()) } + ): Result> { + return try { + Picket( + name, + experimentalData, + modelData, + offsetX, + z, + comment + ).toOkResult() + } catch (e: InvalidPropertiesException) { + e.errors.toErrorResult() + } + } } } \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/geo/ves/Section.kt b/src/main/java/ru/nucodelabs/geo/ves/Section.kt index a3f6d462..0e7fea97 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/Section.kt +++ b/src/main/java/ru/nucodelabs/geo/ves/Section.kt @@ -5,5 +5,7 @@ package ru.nucodelabs.geo.ves * @property pickets Список пикетов для данного разреза */ data class Section( - val pickets: List = listOf() -) + val pickets: List = emptyList() +) : SectionExperimentalDataSet { + override fun pickets(): List = pickets +} diff --git a/src/main/java/ru/nucodelabs/geo/ves/SectionExperimentalDataSet.kt b/src/main/java/ru/nucodelabs/geo/ves/SectionExperimentalDataSet.kt new file mode 100644 index 00000000..1b1ce35e --- /dev/null +++ b/src/main/java/ru/nucodelabs/geo/ves/SectionExperimentalDataSet.kt @@ -0,0 +1,5 @@ +package ru.nucodelabs.geo.ves + +interface SectionExperimentalDataSet { + fun pickets(): List +} \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/geo/ves/calc/ExperimentalDataFunctions.kt b/src/main/java/ru/nucodelabs/geo/ves/calc/ExperimentalDataFunctions.kt index 2cf32645..5bb844df 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/calc/ExperimentalDataFunctions.kt +++ b/src/main/java/ru/nucodelabs/geo/ves/calc/ExperimentalDataFunctions.kt @@ -1,16 +1,15 @@ package ru.nucodelabs.geo.ves.calc import ru.nucodelabs.geo.ves.ExperimentalData +import ru.nucodelabs.geo.ves.ReadOnlyExperimentalSignal -object ExperimentalDataSort { - val orderByDistances = compareBy { it.ab2 }.thenBy { it.mn2 } -} +fun orderByDistances(): Comparator = compareBy { it.ab2 }.thenBy { it.mn2 } fun ExperimentalData.withCalculatedResistanceApparent() = this.copy(resistanceApparent = rhoA(ab2, mn2, amperage, voltage)) -val ExperimentalData.resistanceApparentUpperBoundByError +val ReadOnlyExperimentalSignal.resistanceApparentUpperBoundByError get() = (resistanceApparent + resistanceApparent * errorResistanceApparent / 100).coerceAtLeast(1.0) -val ExperimentalData.resistanceApparentLowerBoundByError +val ReadOnlyExperimentalSignal.resistanceApparentLowerBoundByError get() = (resistanceApparent - resistanceApparent * errorResistanceApparent / 100).coerceAtLeast(1.0) \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/geo/ves/calc/ModelLayerFunctions.kt b/src/main/java/ru/nucodelabs/geo/ves/calc/ModelLayerFunctions.kt index f571c1b3..1ace1d12 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/calc/ModelLayerFunctions.kt +++ b/src/main/java/ru/nucodelabs/geo/ves/calc/ModelLayerFunctions.kt @@ -1,16 +1,15 @@ package ru.nucodelabs.geo.ves.calc import ru.nucodelabs.geo.ves.ModelLayer +import ru.nucodelabs.geo.ves.ReadOnlyModelLayer import ru.nucodelabs.mathves.ModelFunctions fun ModelLayer.divide(): Pair = copy(power = power / 2) to copy(power = power / 2) -fun List.join(): ModelLayer = +fun List.join(): ModelLayer = ModelFunctions.joinLayers( map { it.power }.toDoubleArray(), map { it.resistance }.toDoubleArray() ).let { ModelLayer(power = it[0], resistance = it[1]) - } - -fun joinLayers(layers: List): ModelLayer = layers.join() \ No newline at end of file + } \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/geo/ves/calc/Normalization.kt b/src/main/java/ru/nucodelabs/geo/ves/calc/Normalization.kt index 401872c6..17bcba92 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/calc/Normalization.kt +++ b/src/main/java/ru/nucodelabs/geo/ves/calc/Normalization.kt @@ -1,6 +1,6 @@ package ru.nucodelabs.geo.ves.calc -import ru.nucodelabs.geo.ves.ExperimentalData +import ru.nucodelabs.geo.ves.ReadOnlyExperimentalSignal import ru.nucodelabs.mathves.Normalization import ru.nucodelabs.util.fromPercent @@ -10,7 +10,7 @@ data class FixableValue(val value: T, val isFixed: Boolean) * @return normalized resistance and additive coefficients */ fun normalizeExperimentalData( - experimentalData: List, + experimentalData: List, distinctMn2: List>, idxMap: List ): Pair, List> { @@ -27,7 +27,7 @@ fun normalizeExperimentalData( } @JvmName("distinctMn2ForExpData") -fun distinctMn2(experimentalData: List) = distinctMn2(experimentalData.map { it.mn2 }) +fun distinctMn2(experimentalData: List) = distinctMn2(experimentalData.map { it.mn2 }) fun distinctMn2(mn2: List): Pair, List> { val idx = ShortArray(mn2.size) diff --git a/src/main/java/ru/nucodelabs/geo/ves/calc/PicketFunctions.kt b/src/main/java/ru/nucodelabs/geo/ves/calc/PicketFunctions.kt index 876b5cba..083145c9 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/calc/PicketFunctions.kt +++ b/src/main/java/ru/nucodelabs/geo/ves/calc/PicketFunctions.kt @@ -1,11 +1,12 @@ package ru.nucodelabs.geo.ves.calc -import ru.nucodelabs.geo.ves.Picket +import ru.nucodelabs.geo.ves.ExperimentalDataSet +import ru.nucodelabs.geo.ves.ModelDataSet -fun Picket.zOfModelLayers(): List { +fun ModelDataSet.zOfModelLayers(): List { val heightList: MutableList = mutableListOf() val power = modelData.map { it.power } - var sum = z + var sum = modelZ for (p in power) { sum -= p heightList.add(sum) @@ -18,10 +19,10 @@ fun Picket.zOfModelLayers(): List { * * Тогда верно следующее: * - * `a[i] = j` `<=>` `effectiveExperimentalData[i] = sortedExperimentalData[j]` + * `a[i] = j <=> effectiveExperimentalData[i] = sortedExperimentalData[j]` */ -fun Picket.effectiveToSortedIndicesMapping(): IntArray { - return IntArray(effectiveExperimentalData.size) { i -> - sortedExperimentalData.indexOf(effectiveExperimentalData[i]) - } +fun ExperimentalDataSet.effectiveToSortedIndicesMapping(): IntArray { + val sorted = sortedExperimentalData + val effective = effectiveExperimentalData + return IntArray(effective.size) { i -> sorted.indexOf(effective[i]) } } \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/geo/ves/calc/SectionFunctions.kt b/src/main/java/ru/nucodelabs/geo/ves/calc/SectionFunctions.kt index 350b473e..13386170 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/calc/SectionFunctions.kt +++ b/src/main/java/ru/nucodelabs/geo/ves/calc/SectionFunctions.kt @@ -1,24 +1,16 @@ package ru.nucodelabs.geo.ves.calc -import ru.nucodelabs.geo.ves.Picket -import ru.nucodelabs.geo.ves.Section +import ru.nucodelabs.geo.ves.SectionExperimentalDataSet -fun Section.xOfPicket(picket: Picket): Double = xOfPicket(pickets.indexOf(picket)) - -fun Section.xOfPicket(index: Int): Double { - require(index >= 0) - if (index == 0) return 0.0 - - var x = 0.0 - for (i in 1..index) { - x += this.pickets[i].offsetX - } - return x +fun SectionExperimentalDataSet.xOfPicket(picketIndex: Int): Double { + if (picketIndex == 0) return 0.0 + return pickets().subList(1, picketIndex + 1).map { it.offsetX }.reduce(Double::plus) } data class Bounds(val leftX: Double, val rightX: Double) -fun Section.picketsBounds(): List { +fun SectionExperimentalDataSet.picketsBounds(): List { + val pickets = pickets() if (pickets.isEmpty()) { return emptyList() } @@ -44,7 +36,7 @@ fun Section.picketsBounds(): List { return res } -fun Section.length(): Double { +fun SectionExperimentalDataSet.length(): Double { val bounds = picketsBounds() return bounds.last().rightX - bounds.first().leftX } \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/geo/ves/calc/Ves.kt b/src/main/java/ru/nucodelabs/geo/ves/calc/VesFunctions.kt similarity index 94% rename from src/main/java/ru/nucodelabs/geo/ves/calc/Ves.kt rename to src/main/java/ru/nucodelabs/geo/ves/calc/VesFunctions.kt index 3ecab934..c53c7b99 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/calc/Ves.kt +++ b/src/main/java/ru/nucodelabs/geo/ves/calc/VesFunctions.kt @@ -1,3 +1,4 @@ +@file:JvmName("VesFunctions") package ru.nucodelabs.geo.ves.calc import kotlin.math.PI diff --git a/src/main/java/ru/nucodelabs/geo/ves/calc/adapter/ForwardSolverAdapter.kt b/src/main/java/ru/nucodelabs/geo/ves/calc/adapter/ForwardSolverAdapter.kt index d5b9f8e2..2efb8329 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/calc/adapter/ForwardSolverAdapter.kt +++ b/src/main/java/ru/nucodelabs/geo/ves/calc/adapter/ForwardSolverAdapter.kt @@ -1,10 +1,13 @@ package ru.nucodelabs.geo.ves.calc.adapter import ru.nucodelabs.geo.forward.ForwardSolver -import ru.nucodelabs.geo.ves.ExperimentalData -import ru.nucodelabs.geo.ves.ModelLayer +import ru.nucodelabs.geo.ves.ReadOnlyExperimentalSignal +import ru.nucodelabs.geo.ves.ReadOnlyModelLayer -operator fun ForwardSolver.invoke(experimentalData: List, modelData: List): List { +operator fun ForwardSolver.invoke( + experimentalData: List, + modelData: List +): List { return this( experimentalData.map { it.ab2 }, experimentalData.map { it.mn2 }, diff --git a/src/main/java/ru/nucodelabs/geo/ves/calc/graph/CurvesSectionGraphContext.kt b/src/main/java/ru/nucodelabs/geo/ves/calc/graph/CurvesSectionGraphContext.kt index e0e0b3e8..4aa338c3 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/calc/graph/CurvesSectionGraphContext.kt +++ b/src/main/java/ru/nucodelabs/geo/ves/calc/graph/CurvesSectionGraphContext.kt @@ -1,15 +1,12 @@ package ru.nucodelabs.geo.ves.calc.graph -import jakarta.inject.Inject -import ru.nucodelabs.geo.ves.Section +import ru.nucodelabs.geo.ves.SectionExperimentalDataSet import ru.nucodelabs.geo.ves.calc.Bounds import ru.nucodelabs.geo.ves.calc.picketsBounds import ru.nucodelabs.util.Point import kotlin.math.min -class CurvesSectionGraphContext @Inject constructor(inputSection: Section) { - - private val section = inputSection.copy() +class CurvesSectionGraphContext(private val section: SectionExperimentalDataSet) { private val rightK = 1.0 @@ -27,15 +24,16 @@ class CurvesSectionGraphContext @Inject constructor(inputSection: Section) { fun getPoints(): List> { val pointsList: MutableList> = arrayListOf() - for (picketIdx in section.pickets.indices) { - if (section.pickets[picketIdx].effectiveExperimentalData.isEmpty()) { + val pickets = section.pickets() + for (picketIdx in pickets.indices) { + if (pickets[picketIdx].effectiveExperimentalData.isEmpty()) { pointsList.add(arrayListOf()) continue } pointsList.add(arrayListOf()) for (abIdx in resistances[picketIdx].indices) { val xValue = resistances[picketIdx][abIdx] - val yValue = section.pickets[picketIdx].effectiveExperimentalData[abIdx].ab2 + val yValue = pickets[picketIdx].effectiveExperimentalData[abIdx].ab2 pointsList[picketIdx].add(Point(xValue, yValue)) } } @@ -43,27 +41,33 @@ class CurvesSectionGraphContext @Inject constructor(inputSection: Section) { } private fun addXValues() { + val bounds = section.picketsBounds() for (picketIdx in resistances.indices) { - resistances[picketIdx] = - resistances[picketIdx].map { e -> e + section.picketsBounds()[picketIdx].leftX } as MutableList + val resistSrc = resistances[picketIdx] + resistances[picketIdx] = resistSrc.mapTo(ArrayList(resistSrc.size)) { e -> + e + bounds[picketIdx].leftX + } } } private fun recalculateResistances() { for (picketIdx in resistances.indices) { - resistances[picketIdx] = - resistances[picketIdx].map { e -> e * resistanceK * rightK } as MutableList + val resistSrc = resistances[picketIdx] + resistances[picketIdx] = resistSrc.mapTo(ArrayList(resistSrc.size)) { e -> + e * resistanceK * rightK + } } } private fun initResistances() { - for (picketIdx in section.pickets.indices) { - if (section.pickets[picketIdx].effectiveExperimentalData.isEmpty()) { + val pickets = section.pickets() + for (picketIdx in pickets.indices) { + if (pickets[picketIdx].effectiveExperimentalData.isEmpty()) { resistances.add(arrayListOf()) continue } resistances.add(arrayListOf()) - for (ab in section.pickets[picketIdx].effectiveExperimentalData) { + for (ab in pickets[picketIdx].effectiveExperimentalData) { resistances[picketIdx].add(ab.resistanceApparent) } } @@ -71,18 +75,20 @@ class CurvesSectionGraphContext @Inject constructor(inputSection: Section) { private fun shiftResistances() { for (picketIdx in resistances.indices) { - if (resistances[picketIdx].isEmpty()) - continue - val minResistance = resistances[picketIdx].min() - resistances[picketIdx] = resistances[picketIdx].map { e -> e - minResistance } as MutableList + val resistSrc = resistances[picketIdx] + if (resistSrc.isEmpty()) continue + val minResistance = resistSrc.min() + resistances[picketIdx] = resistSrc.mapTo(ArrayList(resistSrc.size)) { e -> + e - minResistance + } } } private fun setK() { + val picketsBounds = section.picketsBounds() for (picketIdx in resistances.indices) { - if (resistances[picketIdx].isEmpty()) - continue - resistanceK = min(resistanceK, getKFor(resistances[picketIdx], section.picketsBounds()[picketIdx])) + if (resistances[picketIdx].isEmpty()) continue + resistanceK = min(resistanceK, getKFor(resistances[picketIdx], picketsBounds[picketIdx])) } } diff --git a/src/main/java/ru/nucodelabs/geo/ves/calc/graph/MathVesNativeMisfitsFunction.kt b/src/main/java/ru/nucodelabs/geo/ves/calc/graph/MathVesNativeMisfitsFunction.kt index 64af54a6..3de4ea9f 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/calc/graph/MathVesNativeMisfitsFunction.kt +++ b/src/main/java/ru/nucodelabs/geo/ves/calc/graph/MathVesNativeMisfitsFunction.kt @@ -1,15 +1,18 @@ package ru.nucodelabs.geo.ves.calc.graph import ru.nucodelabs.geo.forward.ForwardSolver -import ru.nucodelabs.geo.ves.ExperimentalData -import ru.nucodelabs.geo.ves.ModelLayer +import ru.nucodelabs.geo.ves.ReadOnlyExperimentalSignal +import ru.nucodelabs.geo.ves.ReadOnlyModelLayer import ru.nucodelabs.geo.ves.calc.adapter.invoke import kotlin.math.abs import kotlin.math.sign internal class MathVesNativeMisfitsFunction(val forwardSolver: ForwardSolver) : MisfitsFunction { - override fun invoke(experimentalData: List, modelData: List): List { + override fun invoke( + experimentalData: List, + modelData: List + ): List { if (experimentalData.isEmpty() || modelData.isEmpty()) { return listOf() } diff --git a/src/main/java/ru/nucodelabs/geo/ves/calc/graph/MisfitsFunction.kt b/src/main/java/ru/nucodelabs/geo/ves/calc/graph/MisfitsFunction.kt index 2b49f9d5..f3ef6243 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/calc/graph/MisfitsFunction.kt +++ b/src/main/java/ru/nucodelabs/geo/ves/calc/graph/MisfitsFunction.kt @@ -1,11 +1,14 @@ package ru.nucodelabs.geo.ves.calc.graph -import ru.nucodelabs.geo.ves.ExperimentalData -import ru.nucodelabs.geo.ves.ModelLayer +import ru.nucodelabs.geo.ves.ReadOnlyExperimentalSignal +import ru.nucodelabs.geo.ves.ReadOnlyModelLayer interface MisfitsFunction { /** * Returns list of misfits between experimental and theoretical curves */ - operator fun invoke(experimentalData: List, modelData: List): List + operator fun invoke( + experimentalData: List, + modelData: List + ): List } \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/geo/ves/calc/graph/VesCurvesFunctions.kt b/src/main/java/ru/nucodelabs/geo/ves/calc/graph/VesCurvesFunctions.kt index 91f5c2db..e9253e3d 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/calc/graph/VesCurvesFunctions.kt +++ b/src/main/java/ru/nucodelabs/geo/ves/calc/graph/VesCurvesFunctions.kt @@ -1,39 +1,41 @@ package ru.nucodelabs.geo.ves.calc.graph import ru.nucodelabs.geo.forward.ForwardSolver -import ru.nucodelabs.geo.ves.Picket +import ru.nucodelabs.geo.ves.ExperimentalDataSet +import ru.nucodelabs.geo.ves.ModelDataSet import ru.nucodelabs.geo.ves.calc.adapter.invoke import ru.nucodelabs.geo.ves.calc.resistanceApparentLowerBoundByError import ru.nucodelabs.geo.ves.calc.resistanceApparentUpperBoundByError import ru.nucodelabs.util.Point -fun experimentalCurve(picket: Picket) = +fun experimentalCurve(picket: ExperimentalDataSet) = picket.effectiveExperimentalData.map { Point(it.ab2, it.resistanceApparent) } - -fun experimentalCurveErrorUpperBound(picket: Picket) = +fun experimentalCurveErrorUpperBound(picket: ExperimentalDataSet) = picket.effectiveExperimentalData.map { Point(it.ab2, it.resistanceApparentUpperBoundByError) } -fun experimentalCurveErrorLowerBound(picket: Picket) = +fun experimentalCurveErrorLowerBound(picket: ExperimentalDataSet) = picket.effectiveExperimentalData.map { Point(it.ab2, it.resistanceApparentLowerBoundByError) } -fun experimentalHiddenPoints(picket: Picket) = +fun experimentalHiddenPoints(picket: ExperimentalDataSet) = picket.sortedExperimentalData.filter { it.isHidden }.map { Point(it.ab2, it.resistanceApparent) } -fun theoreticalCurve(picket: Picket, forwardSolver: ForwardSolver): List { - if (picket.sortedExperimentalData.isEmpty() || picket.modelData.isEmpty()) { +fun theoreticalCurve( + experimental: ExperimentalDataSet, + modelDataSet: ModelDataSet, + forwardSolver: ForwardSolver +): List { + val sortedExp = experimental.sortedExperimentalData + if (sortedExp.isEmpty() || modelDataSet.modelData.isEmpty()) { return listOf() } - val solvedResistance = forwardSolver(picket.sortedExperimentalData, picket.modelData) - return List(picket.sortedExperimentalData.size) { i -> - Point(picket.sortedExperimentalData[i].ab2, solvedResistance[i]) + val solvedResistance = forwardSolver(sortedExp, modelDataSet.modelData) + return List(sortedExp.size) { i -> + Point(sortedExp[i].ab2, solvedResistance[i]) } } -fun misfits(picket: Picket, misfitsFunction: MisfitsFunction): List = - misfitsFunction(picket.effectiveExperimentalData, picket.modelData) - -fun modelStepGraph(picket: Picket, beginX: Double = 1e-3, endX: Double = 1e100): List { +fun modelStepGraph(picket: ModelDataSet, beginX: Double = 1e-3, endX: Double = 1e100): List { val modelData = picket.modelData if (modelData.isEmpty()) { diff --git a/src/main/java/ru/nucodelabs/geo/ves/calc/initialModel/MultiLayerInitialModel.kt b/src/main/java/ru/nucodelabs/geo/ves/calc/initialModel/MultiLayerInitialModel.kt index 9dd016f0..f1dd9957 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/calc/initialModel/MultiLayerInitialModel.kt +++ b/src/main/java/ru/nucodelabs/geo/ves/calc/initialModel/MultiLayerInitialModel.kt @@ -4,8 +4,8 @@ import ru.nucodelabs.geo.forward.ForwardSolver import ru.nucodelabs.geo.forward.impl.SonetForwardSolverAdapter import ru.nucodelabs.geo.target.RelativeErrorAwareTargetFunction import ru.nucodelabs.geo.target.impl.SquareDiffTargetFunction -import ru.nucodelabs.geo.ves.ExperimentalData import ru.nucodelabs.geo.ves.ModelLayer +import ru.nucodelabs.geo.ves.ReadOnlyExperimentalSignal import ru.nucodelabs.geo.ves.calc.adapter.invoke import ru.nucodelabs.geo.ves.calc.divide import ru.nucodelabs.geo.ves.calc.inverse.InverseSolver @@ -17,7 +17,7 @@ const val MAX_LAYERS_COUNT = 10 const val MAX_EVAL_INVERSE = 10_000_000 const val MIN_TARGET_FUNCTION_VALUE = 1.0 -private fun initialModel(signals: List): List = +private fun initialModel(signals: List): List = listOf( ModelLayer( power = 0.0, @@ -28,7 +28,7 @@ private fun initialModel(signals: List): List = // сопр среднее по логарифму, потом обратно в степень, с 1 слоем fun multiLayerInitialModel( - signals: List, + signals: List, initialModel: List = initialModel(signals), forwardSolver: ForwardSolver = SonetForwardSolverAdapter(), targetFunction: RelativeErrorAwareTargetFunction = SquareDiffTargetFunction(), diff --git a/src/main/java/ru/nucodelabs/geo/ves/calc/initialModel/SimpleInitialModel.java b/src/main/java/ru/nucodelabs/geo/ves/calc/initialModel/SimpleInitialModel.java index 7cc2979b..69c545ad 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/calc/initialModel/SimpleInitialModel.java +++ b/src/main/java/ru/nucodelabs/geo/ves/calc/initialModel/SimpleInitialModel.java @@ -2,6 +2,7 @@ import ru.nucodelabs.geo.ves.ExperimentalData; import ru.nucodelabs.geo.ves.ModelLayer; +import ru.nucodelabs.geo.ves.ReadOnlyExperimentalSignal; import java.util.ArrayList; import java.util.List; @@ -13,17 +14,24 @@ public class SimpleInitialModel { private SimpleInitialModel() { } - public static List threeLayersInitialModel(List experimentalData) { + public static List threeLayersInitialModel(List experimentalData) { if (experimentalData.size() <= 3) { throw new IllegalStateException("Для построения стартовой модели требуется ≥ 4 измерений, было " + experimentalData.size()); } List logExperimentalData = experimentalData.stream() - .map(data -> copy(data).ab2(Math.log(data.getAb2())).build()) + .map(data -> new ExperimentalData( + Math.log(data.getAb2()), + data.getMn2(), + data.getAmperage(), + data.getVoltage(), + data.getResistanceApparent(), + data.getErrorResistanceApparent(), + data.isHidden() + )) .toList(); - int pointsCnt = logExperimentalData.size(); - double ab2min = logExperimentalData.get(0).getAb2(); - double ab2max = logExperimentalData.get(pointsCnt - 1).getAb2(); + double ab2min = logExperimentalData.getFirst().getAb2(); + double ab2max = logExperimentalData.getLast().getAb2(); double ab2range = ab2max - ab2min; List> logSplitData = new ArrayList<>(); logSplitData.add(logExperimentalData.stream() @@ -51,14 +59,14 @@ public static List threeLayersInitialModel(List ex .average() .orElse(0); if (i > 0) { - prevLast = Math.exp(logSplitData.get(i - 1).get(logSplitData.get(i - 1).size() - 1).getAb2()); + prevLast = Math.exp(logSplitData.get(i - 1).getLast().getAb2()); } else { prevLast = 0; } //От последнего в этом слою отнимаем последний в прошлом - modelLayers.add(new ModelLayer(Math.exp(list.get(list.size() - 1).getAb2()) - prevLast, avg, false, false)); + modelLayers.add(new ModelLayer(Math.exp(list.getLast().getAb2()) - prevLast, avg, false, false)); } - modelLayers.set(modelLayers.size() - 1, copy(modelLayers.get(modelLayers.size() - 1)).power(0).build()); + modelLayers.set(modelLayers.size() - 1, copy(modelLayers.getLast()).power(0).build()); return modelLayers; } } diff --git a/src/main/java/ru/nucodelabs/geo/ves/calc/inverse/InverseSolver.java b/src/main/java/ru/nucodelabs/geo/ves/calc/inverse/InverseSolver.java index e86ef455..c569faab 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/calc/inverse/InverseSolver.java +++ b/src/main/java/ru/nucodelabs/geo/ves/calc/inverse/InverseSolver.java @@ -11,8 +11,9 @@ import org.apache.commons.math3.optim.nonlinear.scalar.noderiv.SimplexOptimizer; import ru.nucodelabs.geo.forward.ForwardSolver; import ru.nucodelabs.geo.target.RelativeErrorAwareTargetFunction; -import ru.nucodelabs.geo.ves.ExperimentalData; import ru.nucodelabs.geo.ves.ModelLayer; +import ru.nucodelabs.geo.ves.ReadOnlyExperimentalSignal; +import ru.nucodelabs.geo.ves.ReadOnlyModelLayer; import ru.nucodelabs.geo.ves.calc.inverse.func.FunctionValue; import java.util.ArrayList; @@ -80,20 +81,20 @@ else if (powers.get(i) > maxPower) } public List getOptimizedModelData( - List experimentalData, - List modelData, + List experimentalData, + List modelData, final int maxEval ) { //Изменяемые сопротивления и мощности List modelResistance = modelData.stream() - .filter(modelLayer -> !modelLayer.isFixedResistance()).map(ModelLayer::getResistance).collect(Collectors.toList()); + .filter(modelLayer -> !modelLayer.isFixedResistance()).map(ReadOnlyModelLayer::getResistance).collect(Collectors.toList()); List modelPower = modelData.stream() - .filter(modelLayer -> !modelLayer.isFixedPower()).map(ModelLayer::getPower).collect(Collectors.toList()); + .filter(modelLayer -> !modelLayer.isFixedPower()).map(ReadOnlyModelLayer::getPower).collect(Collectors.toList()); //Установка ограничений для адекватности обратной задачи double maxPower = experimentalData.stream() - .map(ExperimentalData::getAb2) + .map(ReadOnlyExperimentalSignal::getAb2) .mapToDouble(Double::doubleValue) .max() .orElseThrow(); @@ -105,9 +106,9 @@ public List getOptimizedModelData( //Неизменяемые сопротивления и мощности List fixedModelResistance = modelData.stream() - .filter(ModelLayer::isFixedResistance).map(ModelLayer::getResistance).toList(); + .filter(ReadOnlyModelLayer::isFixedResistance).map(ReadOnlyModelLayer::getResistance).toList(); List fixedModelPower = modelData.stream() - .filter(ModelLayer::isFixedPower).map(ModelLayer::getPower).toList(); + .filter(ReadOnlyModelLayer::isFixedPower).map(ReadOnlyModelLayer::getPower).toList(); SimplexOptimizer optimizer = new SimplexOptimizer(relativeThreshold, absoluteThreshold); @@ -148,7 +149,7 @@ public List getOptimizedModelData( int cntFixedResistances = 0; int cntUnfixedResistances = 0; - for (ModelLayer modelLayer : modelData) { + for (var modelLayer : modelData) { if (modelLayer.isFixedResistance()) { newModelResistance.add(fixedModelResistance.get(cntFixedResistances)); cntFixedResistances++; @@ -161,7 +162,7 @@ public List getOptimizedModelData( int cntFixedPowers = 0; int cntUnfixedPowers = 0; for (int i = 0; i < modelData.size() - 1; i++) { - ModelLayer modelLayer = modelData.get(i); + var modelLayer = modelData.get(i); int shift = modelResistance.size(); if (modelLayer.isFixedPower()) { newModelPower.add(fixedModelPower.get(cntFixedPowers)); diff --git a/src/main/java/ru/nucodelabs/geo/ves/calc/inverse/InverseSolverAdapter.kt b/src/main/java/ru/nucodelabs/geo/ves/calc/inverse/InverseSolverAdapter.kt index 1fbaaad6..e5d5749b 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/calc/inverse/InverseSolverAdapter.kt +++ b/src/main/java/ru/nucodelabs/geo/ves/calc/inverse/InverseSolverAdapter.kt @@ -1,10 +1,11 @@ package ru.nucodelabs.geo.ves.calc.inverse -import ru.nucodelabs.geo.ves.ExperimentalData import ru.nucodelabs.geo.ves.ModelLayer +import ru.nucodelabs.geo.ves.ReadOnlyExperimentalSignal +import ru.nucodelabs.geo.ves.ReadOnlyModelLayer operator fun InverseSolver.invoke( - experimentalSignals: List, - initialModel: List, + experimentalSignals: List, + initialModel: List, maxEval: Int = InverseSolver.MAX_EVAL_DEFAULT ): List = getOptimizedModelData(experimentalSignals, initialModel, maxEval) \ No newline at end of file diff --git a/src/main/java/ru/nucodelabs/geo/ves/calc/inverse/func/FunctionValue.java b/src/main/java/ru/nucodelabs/geo/ves/calc/inverse/func/FunctionValue.java index 71465f6e..1e272727 100644 --- a/src/main/java/ru/nucodelabs/geo/ves/calc/inverse/func/FunctionValue.java +++ b/src/main/java/ru/nucodelabs/geo/ves/calc/inverse/func/FunctionValue.java @@ -3,8 +3,9 @@ import org.apache.commons.math3.analysis.MultivariateFunction; import ru.nucodelabs.geo.forward.ForwardSolver; import ru.nucodelabs.geo.target.RelativeErrorAwareTargetFunction; -import ru.nucodelabs.geo.ves.ExperimentalData; import ru.nucodelabs.geo.ves.ModelLayer; +import ru.nucodelabs.geo.ves.ReadOnlyExperimentalSignal; +import ru.nucodelabs.geo.ves.ReadOnlyModelLayer; import ru.nucodelabs.geo.ves.calc.adapter.ForwardSolverAdapterKt; import java.util.ArrayList; @@ -12,18 +13,18 @@ public class FunctionValue implements MultivariateFunction { //Экспериментальные точки для FS - private final List experimentalData; + private final List experimentalData; //Функция для вычисления разности между exp и theoretical точками private final RelativeErrorAwareTargetFunction targetFunction; //Исходная модель - private final List modelLayers; + private final List modelLayers; private final ForwardSolver forwardSolver; private double diffMinValue = Double.MAX_VALUE; - public FunctionValue(List experimentalData, + public FunctionValue(List experimentalData, RelativeErrorAwareTargetFunction targetFunction, - List modelLayers, + List modelLayers, ForwardSolver forwardSolver) { this.experimentalData = experimentalData; this.targetFunction = targetFunction; @@ -54,7 +55,7 @@ public double value(double[] variables) { List newModelPower = new ArrayList<>(); int cntUnfixedResistances = 0; - for (ModelLayer modelLayer : modelLayers) { + for (var modelLayer : modelLayers) { if (modelLayer.isFixedResistance()) { newModelResistance.add(modelLayer.getResistance()); } else { @@ -64,7 +65,7 @@ public double value(double[] variables) { } int cntUnfixedPowers = 0; - for (ModelLayer modelLayer : modelLayers) { + for (var modelLayer : modelLayers) { if (modelLayer.isFixedPower()) { newModelPower.add(modelLayer.getPower()); } else { @@ -82,8 +83,8 @@ public double value(double[] variables) { double diffValue = targetFunction.invoke( solvedResistance, - experimentalData.stream().map(ExperimentalData::getResistanceApparent).toList(), - experimentalData.stream().map(ExperimentalData::getErrorResistanceApparent).toList() + experimentalData.stream().map(ReadOnlyExperimentalSignal::getResistanceApparent).toList(), + experimentalData.stream().map(ReadOnlyExperimentalSignal::getErrorResistanceApparent).toList() ); boolean flag = false; @@ -92,7 +93,7 @@ public double value(double[] variables) { if (modelLayer.getResistance() < 0.1 || modelLayer.getResistance() > 1e5 || (modelLayer.getPower() != 0.0 && modelLayer.getPower() < 0.1) || - modelLayer.getPower() > experimentalData.get(experimentalData.size() - 1).getAb2()) { + modelLayer.getPower() > experimentalData.getLast().getAb2()) { diffValue = Math.max(diffMinValue * (1.1 + 0.1 * Math.random()), diffValue); flag = true; break; diff --git a/src/main/resources/ru/nucodelabs/gem/view/controller/charts/VesCurves.fxml b/src/main/resources/ru/nucodelabs/gem/view/controller/charts/VesCurves.fxml index ed467c56..2fa49bd3 100644 --- a/src/main/resources/ru/nucodelabs/gem/view/controller/charts/VesCurves.fxml +++ b/src/main/resources/ru/nucodelabs/gem/view/controller/charts/VesCurves.fxml @@ -13,6 +13,7 @@ maxWidth="Infinity" stylesheets="/css/common.css, @VesCurves.css" VBox.vgrow="ALWAYS" + fx:id="root" fx:controller="ru.nucodelabs.gem.view.controller.charts.VesCurvesController">