001package conexp.fx.core.layout;
002
003/*
004 * #%L
005 * Concept Explorer FX
006 * %%
007 * Copyright (C) 2010 - 2023 Francesco Kriegel
008 * %%
009 * This program is free software: you can redistribute it and/or modify
010 * it under the terms of the GNU General Public License as
011 * published by the Free Software Foundation, either version 3 of the
012 * License, or (at your option) any later version.
013 * 
014 * This program is distributed in the hope that it will be useful,
015 * but WITHOUT ANY WARRANTY; without even the implied warranty of
016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
017 * GNU General Public License for more details.
018 * 
019 * You should have received a copy of the GNU General Public
020 * License along with this program.  If not, see
021 * <http://www.gnu.org/licenses/gpl-3.0.html>.
022 * #L%
023 */
024
025import java.util.Map;
026import java.util.Queue;
027import java.util.concurrent.ConcurrentHashMap;
028import java.util.concurrent.ConcurrentLinkedQueue;
029
030import conexp.fx.core.collections.ListIterators;
031import conexp.fx.core.collections.Pair;
032import conexp.fx.core.context.Concept;
033import conexp.fx.core.context.ConceptLattice;
034import conexp.fx.core.math.Points;
035import javafx.beans.property.SimpleObjectProperty;
036import javafx.geometry.BoundingBox;
037import javafx.geometry.Point3D;
038
039public class SimpleConceptLayout<G, M> extends ConceptLayout<G, M, SimpleObjectProperty<Point3D>> {
040
041  public SimpleConceptLayout(ConceptLattice<G, M> conceptLattice) {
042    super(conceptLattice);
043    start();
044  }
045
046  @Override
047  protected SimpleObjectProperty<Point3D> newPositionBinding(Concept<G, M> concept) {
048    return new SimpleObjectProperty<Point3D>(Point3D.ZERO);
049  }
050
051  @Override
052  public void rotate(double angle) {
053    // TODO Auto-generated method stub
054  }
055
056  @Override
057  public void move(Concept<G, M> concept, ConceptMovement movement, Point3D delta) {
058    // TODO Auto-generated method stub
059    synchronized (moves) {
060      moves.offer(Pair.of(concept, delta));
061    }
062//    final SimpleObjectProperty<Point3D> p = getOrAddPosition(concept);
063//    double y = Double.MAX_VALUE;
064//    for (Concept<G, M> upper : lattice.upperNeighbors(concept)) {
065//      final SimpleObjectProperty<Point3D> q = getOrAddPosition(upper);
066//      y = Math.min(y, q.getValue().subtract(p.getValue()).getY());
067//    }
068//    p.setValue(p.getValue().add(new Point3D(delta.getX(), Math.min(delta.getY(), -y), delta.getZ())));
069//    p.setValue(p.getValue().add(delta));
070//    final Set<Concept<G, M>> filter = lattice.filter(concept);
071//    final double factor = 1d / (double) filter.size();
072//    filter.forEach(upper -> {
073//      final SimpleObjectProperty<Point3D> p = getOrAddPosition(upper);
074//      p.setValue(p.getValue().add(delta.multiply(factor)));
075//    });
076//    invalidate();
077  }
078
079  private final void _move(final Concept<G, M> concept, final Point3D delta) {
080    final SimpleObjectProperty<Point3D> p = getOrAddPosition(concept);
081    p.setValue(p.getValue().add(delta));
082    invalidate();
083  }
084
085  @Override
086  public void deleteZ() {
087    // TODO Auto-generated method stub
088  }
089
090  private final Queue<Pair<Concept<G, M>, Point3D>> moves = new ConcurrentLinkedQueue<>();
091  private Thread                                    thread;
092  private final long                                delay = 400l;
093
094  public final void start() {
095    synchronized (this) {
096      if (thread == null) {
097        thread = new Thread(() -> {
098          try {
099            while (true) {
100//              if (ConExpFX.instance.executor.isIdleBinding.get())
101              evolveLayout();
102              Thread.sleep(delay);
103            }
104          } catch (InterruptedException e) {
105            System.err.println("Force interrupted.");
106          }
107        }, SimpleConceptLayout.class.getName());
108        thread.start();
109      }
110    }
111  }
112
113  public final void stop() {
114    synchronized (this) {
115      if (thread != null) {
116        thread.interrupt();
117        thread = null;
118      }
119    }
120  }
121
122  private final void evolveLayout() {
123//    normalizeSeeds();
124//    double size = getMaximalNodeDistance();
125//    if (size == 0d)
126//      size = 1d;
127    final double k = getK();
128    final Map<Concept<G, M>, Point3D> deltas = new ConcurrentHashMap<>();
129    lattice.rowHeads().forEach(c -> deltas.put(c, Point3D.ZERO));
130    synchronized (positionBindings) {
131      computeAdjacentNodeForces(k, deltas);
132      computeNodeForces(k, deltas);
133//    computeNodeEdgeForces(k, deltas);
134//    adjustWH(deltas);
135      synchronized (moves) {
136        for (Pair<Concept<G, M>, Point3D> move = moves.poll(); move != null; move = moves.poll()) {
137          final Pair<Concept<G, M>, Point3D> _move = move;
138          deltas.compute(_move.x(), (c, d) -> d.add(_move.y()));
139        }
140      }
141      deltas.replaceAll((c, d) -> {
142        if (Double.isFinite(d.getX()) && Double.isFinite(d.getY()) && Double.isFinite(d.getZ()))
143          return d;
144        else
145          return new Point3D(Math.random(), Math.random(), 0d).normalize();
146      });
147      deltas.forEach((concept, delta) -> _move(concept, delta));
148    }
149  }
150
151  private final double getMaximalNodeDistance() {
152    double size = 0d;
153    for (Concept<G, M> c1 : lattice.rowHeads())
154      for (Concept<G, M> c2 : lattice.rowHeads()) {
155        size = Math.max(size, getPosition(c1).getValue().subtract(getPosition(c2).getValue()).magnitude());
156      }
157    return size;
158  }
159
160  private final double getK() {
161    final BoundingBox box = getCurrentBoundingBox(false, false);
162    final double area = box.getWidth() * box.getHeight();
163    final double n = lattice.rowHeads().size();
164    return Math.sqrt(area / n);
165  }
166
167  private final void adjustWH(final Map<Concept<G, M>, Point3D> deltas) {
168    final BoundingBox box = getCurrentBoundingBox(false, false);
169    final double wh = box.getWidth() / box.getHeight();
170    deltas.replaceAll((__, delta) -> new Point3D(delta.getX(), delta.getY() * wh, delta.getZ()));
171  }
172
173  private final void computeAdjacentNodeForces(final double k, final Map<Concept<G, M>, Point3D> deltas) {
174    for (Pair<Concept<G, M>, Concept<G, M>> edge : lattice)
175//      if (!edge.x().extent().containsAll(context.rowHeads())
176//          && !edge.x().intent().containsAll(context.colHeads())
177//          && !edge.y().extent().containsAll(context.rowHeads())
178//          && !edge.y().intent().containsAll(context.colHeads())) 
179    {
180      Point3D delta1 = deltas.get(edge.x());
181      Point3D delta2 = deltas.get(edge.y());
182      final Point3D q1 = getOrAddPosition(edge.x()).getValue();
183      final Point3D q2 = getOrAddPosition(edge.y()).getValue();
184      final Point3D q = q2.subtract(q1);
185      final double x = Math.log1p(q.magnitude()) / k;
186//      final double x = Math.pow(q.magnitude(), 2d) / k;
187      final Point3D q0 = q.normalize().multiply(x);
188      delta1 = delta1.add(q0);
189      delta2 = delta2.subtract(q0);
190      if (q.getY() > 0) {
191        final double dx = -q.getY();
192        delta2.subtract(0d, dx, 0d);
193        delta1.add(0d, dx, 0d);
194      }
195      deltas.put(edge.x(), delta1);
196      deltas.put(edge.y(), delta2);
197    }
198  }
199
200  private final void computeNodeForces(final double k, final Map<Concept<G, M>, Point3D> deltas) {
201    for (Pair<Concept<G, M>, Concept<G, M>> pair : ListIterators.upperCartesianDiagonalStrict(lattice.rowHeads())) {
202      Point3D delta1 = deltas.get(pair.x());
203      Point3D delta2 = deltas.get(pair.y());
204      final Point3D q1 = getOrAddPosition(pair.x()).getValue();
205      final Point3D q2 = getOrAddPosition(pair.y()).getValue();
206      final Point3D q = q2.subtract(q1);
207//    if pair.x is a subconcept of pair.y, then q1 must be below q2, i.e., q1.y>q2.y
208//      if this is not the case, we decrease q2.y and increase q1.y
209//      if () {// || q.getY() > 0.8d * q.magnitude()) {
210//      final int c = lattice.contains(pair.x(), pair.y()) ? -1 : lattice.contains(pair.y(), pair.x()) ? 1 : 0;
211//      if (c != 0&&q.getY()<0) {
212//      final int sx = pair.x().extent().size();
213//      final int sy = pair.y().extent().size();
214//      final double df = sx == sy || q.getY() == 0d ? 0d : sx < sy == q.getY() < 0d ? 0d : 1d;
215//      final double dy = df * Math.abs(q.getY());
216////      final double dy = df * Math.log1p(1d + Math.abs(q.getY()));
217//      delta1 = delta1.add(0d, dy, 0d);
218//      delta2 = delta2.subtract(0d, dy, 0d);
219//      }
220//      }
221      final double x = Math.pow(k, 2d) / q.magnitude();
222      final Point3D q0 = q.normalize().multiply(x);
223      delta1 = delta1.subtract(q0);
224      delta2 = delta2.add(q0);
225      deltas.put(pair.x(), delta1);
226      deltas.put(pair.y(), delta2);
227    }
228  }
229
230  private final void computeNodeEdgeForces(final double k, final Map<Concept<G, M>, Point3D> deltas) {
231    for (Concept<G, M> concept : lattice.rowHeads())
232    // if (!concept.extent().containsAll(context.rowHeads())
233    // && !concept.intent().containsAll(context.colHeads()))
234    {
235      Point3D delta = deltas.get(concept);
236      final Point3D p = getOrAddPosition(concept).getValue();
237      for (Pair<Concept<G, M>, Concept<G, M>> edge : lattice)
238        if (!edge.x().equals(concept) && !edge.y().equals(concept)) {
239          Point3D delta1 = deltas.get(edge.x());
240          Point3D delta2 = deltas.get(edge.y());
241          final Point3D q1 = getOrAddPosition(edge.x()).getValue();
242          final Point3D q2 = getOrAddPosition(edge.y()).getValue();
243          final Point3D q = Points.shortestVectorFromLineSegment(p, q1, q2);
244          final double x = Math.pow(k, 2d) / Math.pow(q.magnitude(), 2d);
245          final Point3D q0 = q.normalize().multiply(x);
246          delta1 = delta1.add(q0.multiply(-0.5d));
247          delta2 = delta2.add(q0.multiply(-0.5d));
248          delta = delta.add(q0.multiply(1d));
249          deltas.put(edge.x(), delta1);
250          deltas.put(edge.y(), delta2);
251        }
252      deltas.put(concept, delta);
253    }
254  }
255
256}