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}