двунаправленный с функцией? - PullRequest
1 голос
/ 21 февраля 2020

Я знаю, что вы можете использовать NumberBindings, скажем, для привязки x к y так, что если x равен 10, то y равен 20. Я хочу сделать что-то более сложное, и я не уверен, что могу использовать привязки для достижения эта цель.

Я хочу иметь два набора из трех ползунков, которые переводят вектор в трехмерном пространстве, в декартовы или сферические координаты. То, что я хотел бы видеть, случается, когда всякий раз, когда изменяется одно из значений в декартовом ползунке, значения на сферических ползунках изменяются соответственно, и наоборот. Таким образом, изменение одного значения в одном наборе из трех приводит к изменению трех значений в другом наборе из трех.

Я хотел бы попробовать это с помощью ChangeListeners, но я попадаю в циклы, где изменение A приводит к пересчету B, приводит к пересчитывая A, и я получаю одноименную ошибку этого сайта. Я также подумал попробовать создать один векторный объект, который имеет двунаправленную привязку к обоим наборам ползунков, но у меня возникает та же проблема с этим. ползунки в одном наборе меняются пользователем по сравнению с изменением слушателем, но я не понял, как это сделать, и даже если это так, я подозреваю, что это может быть неэффективным способом сделать это. Хочу ли я вместо этого использовать ActionListener?

Это то, что я могу сделать?

РЕДАКТИРОВАТЬ: Добавлено MWE

import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ChangeListener;

public class BindingProblemMWE {
    private DoubleProperty xProperty;
    public DoubleProperty xProperty() {
        if (xProperty == null) {xProperty = new SimpleDoubleProperty(0);}
        return xProperty;
    }

    private DoubleProperty yProperty;
    public DoubleProperty yProperty() {
        if (yProperty == null) {yProperty = new SimpleDoubleProperty(0);}
        return yProperty;
    }

    private DoubleProperty zProperty;
    public DoubleProperty zProperty() {
        if (zProperty == null) {zProperty = new SimpleDoubleProperty(0);}
        return zProperty;
    }

    private DoubleProperty azProperty;
    public DoubleProperty azProperty() {
        if (azProperty == null) {azProperty = new SimpleDoubleProperty(0);}
        return azProperty;
    }

    private DoubleProperty elProperty;
    public DoubleProperty elProperty() {
        if (elProperty == null) {elProperty = new SimpleDoubleProperty(0);}
        return elProperty;
    }

    private DoubleProperty rhoProperty;
    public DoubleProperty rhoProperty() {
        if (rhoProperty == null) {rhoProperty = new SimpleDoubleProperty(0);}
        return rhoProperty;
    }

    private ChangeListener<Number> recalculateSpherical() {
        return (obs, ov, nv) -> {

            double x = xProperty().doubleValue();
            double y = yProperty().doubleValue();
            double z = zProperty().doubleValue();

            azProperty.set(calculateAz(x,y,z));
            elProperty.set(calculateEl(x,y,z));
            rhoProperty.set(calculateRho(x,y,z));
        };
    }

    private ChangeListener<Number> recalculateCartesian() {
        return (obs, ov, nv) -> {
            double az = azProperty().doubleValue();
            double el = elProperty().doubleValue();
            double rho = rhoProperty().doubleValue();

            xProperty.set(calculateX(az, el, rho));
            yProperty.set(calculateY(az, el, rho));
            zProperty.set(calculateZ(az, el, rho));
        };
    }

    private void initialize() {

        xProperty().addListener(recalculateSpherical());
        yProperty().addListener(recalculateSpherical());
        zProperty().addListener(recalculateSpherical());

        azProperty().addListener(recalculateCartesian());
        elProperty().addListener(recalculateCartesian());
        rhoProperty().addListener(recalculateCartesian());

        xProperty().set(1);
        yProperty().set(1);
        zProperty().set(1);

    }

    private static double calculateX(double az, double el, double rho) {
        return rho*Math.sin(el)*Math.cos(az);}

    private static double calculateY(double az, double el, double rho) {
        return rho*Math.sin(el)*Math.sin(az);}

    private static double calculateZ(double az, double el, double rho) {
        return rho*Math.cos(el);}

    private static double calculateAz(double x, double y, double z) {
        return Math.atan2(y, x);}

    private static double calculateEl(double x, double y, double z) {
            return Math.atan2(Math.sqrt(x*x + y*y), z);}

    private static double calculateRho(double x, double y, double z) {
        return Math.sqrt(x*x + y*y + z*z);}


    public static void main(String [] args) {
        BindingProblemMWE mwe = new BindingProblemMWE();
        mwe.initialize();
    }
}

1 Ответ

1 голос
/ 21 февраля 2020

Томас Микула имеет библиотеку под названием ReactFX , которая включает в себя версии свойств JavaFX, которые, как правило, работают намного лучше и обладают нужной вам функциональностью. Посмотрите на интерфейсы Val и Var в пакете org.reactfx.value. Похоже, что это не находится в процессе активного обслуживания.

Причина, по которой вы видите исключения переполнения стека в вашем примере, заключается в том, что изменение каждой отдельной координаты вызывает изменение координат в другом представлении. Эти отдельные изменения, конечно же, не являются противоположностями друг другу, поэтому вы в конечном итоге получите бесконечное число изменений oop. (Например, изменение x приводит к изменению az, что приводит к тому, что сферическое представление является точкой, отличной от той, которую представляло изменение в x, так что это приводит к дальнейшему изменению x, et c.) Если вы «распыляете» три изменения в каждом представлении, то теоретически вы останавливаете бесконечную рекурсию, пока две функции являются точными противоположностями друг другу.

Вероятно, лучший способ достичь этого - использовать один объект вместо трех разных значений. Это дает дополнительное преимущество, заключающееся в том, что при изменении точки в трехмерном пространстве вы наблюдаете одно изменение вместо трех. Поэтому здесь я бы использовал ObjectProperty<T> для некоторого подходящего типа T.

Последнее, на что следует обратить внимание: когда вы работаете с арифметикой с плавающей запятой c, как в вашем примере, вы по-прежнему уязвимы к исключениям переполнения стека, поскольку численные ошибки в вычислениях мешают функциям быть точной противоположностью друг другу.
Ваша идея установить и снять флаг, когда они меняются, - правильный подход; Вы просто должны выяснить, где сохранить этот флаг. В этом примере я создаю отдельный класс BidirectionalBinding, который позволяет хранить флаг, а также отсоединять обоих слушателей с помощью метода в классе.

import java.util.function.Function;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;

public class BidirectionalBinding<T,U> {

    private final Property<T> source ;
    private final Property<U> target ;

    private boolean changing = false ;
    private ChangeListener<? super T> sourceListener;
    private ChangeListener<? super U> targetListener;

    public BidirectionalBinding(Property<T> source, Property<U> target, 
            Function<T,U> mapping, Function<U,T> inverseMapping) {
        this.source = source ;
        this.target = target ;

        target.setValue(mapping.apply(source.getValue()));

        sourceListener = (obs, oldSourceValue, newSourceValue) -> {
            if (! changing) {
                changing = true ;
                target.setValue(mapping.apply(newSourceValue));
            }
            changing = false ;
        };
        source.addListener(sourceListener);

        targetListener = (obs, oldTargetValue, newTargetValue) -> {
            if (! changing) {
                changing = true ;
                source.setValue(inverseMapping.apply(newTargetValue));
            }
            changing = false ;
        };
        target.addListener(targetListener);

    }

    public void unbind() {
        source.removeListener(sourceListener);
        target.removeListener(targetListener);
    }

    public Property<T> getSource() {
        return source;
    }

    public Property<U> getTarget() {
        return target;
    }

    public static class Cartesian {
        private final double x ;
        private final double y ;
        private final double z ;
        public Cartesian(double x, double y, double z) {
            super();
            this.x = x;
            this.y = y;
            this.z = z;
        }
        public double getX() {
            return x;
        }
        public double getY() {
            return y;
        }
        public double getZ() {
            return z;
        }
        @Override
        public String toString() {
            return String.format("[x=%f, y=%f, z=%f]", x, y, z);
        }
        @Override
        public boolean equals(Object o) {
            if (! (o instanceof Cartesian)) return false ;
            if (o == this) return true ;
            Cartesian other = (Cartesian) o ;
            return x == other.x && y == other.y && z == other.z ;
        }

    }

    public static class Spherical {
        private final double az ;
        private final double el ;
        private final double rho ;
        public Spherical(double az, double el, double rho) {
            super();
            this.az = az;
            this.el = el;
            this.rho = rho;
        }
        public double getAz() {
            return az;
        }
        public double getEl() {
            return el;
        }
        public double getRho() {
            return rho;
        }
        @Override
        public String toString() {
            return String.format("[az=%f, el=%f, rho=%f]", az, el, rho);
        }
        @Override
        public boolean equals(Object o) {
            if (! (o instanceof Spherical)) return false ;
            if (o == this) return true ;
            Spherical other = (Spherical) o ;
            return az == other.az && el == other.el && rho == other.rho ;
        }           

    }

    // test case:
    public static void main(String[] args) {
        ObjectProperty<Cartesian> cartesian = new SimpleObjectProperty<>(new Cartesian(0,0,0));
        ObjectProperty<Spherical> spherical = new SimpleObjectProperty<>(new Spherical(0,0,0));

        Function<Cartesian, Spherical> cartesianToSpherical = cart -> {
            double x = cart.getX();
            double y = cart.getY();
            double z = cart.getZ();
            double az = Math.atan2(y, x);
            double el = Math.atan2(Math.sqrt(x*x + y*y), z);
            double rho = Math.sqrt(x*x + y*y + z*z) ;
            return new Spherical(az, el, rho);
        };

        Function<Spherical, Cartesian> sphericalToCartesian = spher -> {
            double az = spher.getAz();
            double el = spher.getEl();
            double rho = spher.getRho();
            double x = rho*Math.sin(el)*Math.cos(az);
            double y = rho*Math.sin(el)*Math.sin(az);
            double z = rho*Math.cos(el);
            return new Cartesian(x, y, z);
        };

        BidirectionalBinding<Cartesian, Spherical> binding = new BidirectionalBinding<>(cartesian, spherical,
                cartesianToSpherical, sphericalToCartesian);

        System.out.println(cartesian.get());
        System.out.println(spherical.get());
        System.out.println("\nSetting cartesian to [1,1,1]");
        cartesian.set(new Cartesian(1, 1, 1));
        System.out.println(cartesian.get());
        System.out.println(spherical.get());
        System.out.println("\nSetting sphercial to [pi/4, pi/4, 1]");
        spherical.set(new Spherical(Math.PI/4, Math.PI/4, 1));
        System.out.println(cartesian.get());
        System.out.println(spherical.get());

        binding.unbind();
    }
}

Если вы хотите сохранить три отдельных свойства для три координаты в каждом представлении, тогда я думаю, что вы все еще могли бы использовать эту общую стратегию, но вам, вероятно, понадобится определенный класс c, который принимает шесть свойств в качестве полей и задает c для этой проблемы вместо будучи обобщенным c как в этом примере.

С ReactFX (и классами Cartesian и Spherical, которые я определил), я думаю, вы можете сделать что-то вроде

ObjectProperty<Cartesian> cartesian = new SimpleObjectProperty<>(new Cartesian(0,0,0));
Property<Spherical> = Var.mapBidirectional(cartesian, cartesianToSpherical, sphericalToCartesian);

Если Томас все еще активен на этом сайте, он ' скорее всего, даст вам некоторые дополнительные идеи (и, возможно, более надежные реализации, чем те, которые я предлагаю).

...