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.HashSet;
026import java.util.Map;
027import java.util.NoSuchElementException;
028import java.util.Set;
029import java.util.concurrent.ConcurrentHashMap;
030import java.util.stream.Stream;
031
032import org.eclipse.jdt.annotation.Nullable;
033
034import com.google.common.base.Predicate;
035import com.google.common.collect.Collections2;
036import com.google.common.collect.Iterables;
037import com.google.common.collect.Iterators;
038import com.google.common.collect.Maps;
039import com.google.common.collect.Sets;
040
041import conexp.fx.core.context.Concept;
042import conexp.fx.core.context.ConceptLattice;
043import conexp.fx.core.math.Points;
044import javafx.beans.binding.Binding;
045import javafx.beans.binding.ObjectBinding;
046import javafx.beans.property.Property;
047import javafx.beans.property.SimpleObjectProperty;
048import javafx.beans.value.ObservableValue;
049import javafx.collections.FXCollections;
050import javafx.collections.ObservableMap;
051import javafx.geometry.Point3D;
052
053public final class AdditiveConceptLayout<G, M> extends ConceptLayout<G, M, Binding<Point3D>> {
054
055  public enum Type {
056    ATTRIBUTE("Attribute-Additive"),
057    OBJECT("Object-Additive"),
058    HYBRID("Hybrid-Additive");
059
060    private final String title;
061
062    private Type(final String title) {
063      this.title = title;
064    }
065
066    @Override
067    public final String toString() {
068      return title;
069    }
070  }
071
072  public final ObservableMap<G, Point3D>             seedsG       =
073      FXCollections.observableMap(new ConcurrentHashMap<>());
074  public final Map<G, Point3D>                       seedHistoryG = new ConcurrentHashMap<G, Point3D>();
075
076  public final ObservableMap<M, Point3D>             seedsM       =
077      FXCollections.observableMap(new ConcurrentHashMap<>());
078  public final Map<M, Point3D>                       seedHistoryM = new ConcurrentHashMap<M, Point3D>();
079
080  private final Property<AdditiveConceptLayout.Type> type;
081
082  public AdditiveConceptLayout(
083      final ConceptLattice<G, M> conceptLattice,
084      final @Nullable Map<G, Point3D> initialSeedsG,
085      final @Nullable Map<M, Point3D> initialSeedsM,
086      final AdditiveConceptLayout.Type type) {
087    super(conceptLattice);
088    this.type = new SimpleObjectProperty<>(type);
089    initializePositionBindings();
090    if (initialSeedsG != null)
091      seedsG.putAll(initialSeedsG);
092    if (initialSeedsM != null)
093      seedsM.putAll(initialSeedsM);
094  }
095
096  public final void setType(final AdditiveConceptLayout.Type type) {
097    this.type.setValue(type);
098  }
099
100  public final void bindType(final ObservableValue<AdditiveConceptLayout.Type> observable) {
101    this.type.bind(observable);
102  }
103
104  protected final Binding<Point3D> newPositionBinding(final Concept<G, M> concept) {
105//    synchronized (positionBindings) {
106//      positionBindings.put(concept, 
107    return new ObjectBinding<Point3D>() {
108
109      {
110        if (observe)
111          bind(seedsG, concept.extent(), seedsM, concept.intent(), type);
112        else
113          bind(seedsG, seedsM, type);
114      }
115
116      public final void dispose() {
117        if (observe)
118          unbind(seedsG, concept.extent(), seedsM, concept.intent(), type);
119        else
120          unbind(seedsG, seedsM, type);
121        super.dispose();
122      }
123
124      protected final Point3D computeValue() {
125        double x = 0d;
126        double y = 0d;
127        double z = 0d;
128        if (type.getValue().equals(AdditiveConceptLayout.Type.HYBRID)
129            || type.getValue().equals(AdditiveConceptLayout.Type.OBJECT))
130          synchronized (concept.extent()) {
131            synchronized (seedsG) {
132              for (G g : Sets.difference(seedsG.keySet(), concept.extent())) {
133                final Point3D seed = seedsG.get(g);
134                x += seed.getX();
135                y += seed.getY();
136                z += seed.getZ();
137              }
138            }
139          }
140        if (type.getValue().equals(AdditiveConceptLayout.Type.HYBRID)
141            || type.getValue().equals(AdditiveConceptLayout.Type.ATTRIBUTE))
142          synchronized (concept.intent()) {
143            synchronized (seedsM) {
144              for (M m : Sets.intersection(seedsM.keySet(), concept.intent())) {
145                final Point3D seed = seedsM.get(m);
146                x += seed.getX();
147                y += seed.getY();
148                z += seed.getZ();
149              }
150            }
151          }
152        return new Point3D(x, y, z);
153      }
154//      })
155    };
156  }
157
158  public final boolean
159      updateSeeds(final @Nullable Map<G, Point3D> seedUpdatesG, final @Nullable Map<M, Point3D> seedUpdatesM) {
160    boolean ret = false;
161    if (!seedsG.equals(seedUpdatesG))
162      synchronized (seedsG) {
163        ret = true;
164        seedsG.clear();
165        if (seedUpdatesG != null)
166          seedsG.putAll(seedUpdatesG);
167      }
168    if (!seedsM.equals(seedUpdatesM))
169      synchronized (seedsM) {
170        ret = true;
171        seedsM.clear();
172        if (seedUpdatesM != null)
173          seedsM.putAll(seedUpdatesM);
174      }
175    invalidate();
176    return ret;
177  }
178
179  public final void normalizeSeeds() {
180    final double l = 1d / Stream
181        .concat(seedsG.values().stream(), seedsM.values().stream())
182        .map(Point3D::magnitude)
183        .reduce(Math::min)
184        .orElse(1d);
185    seedsG.replaceAll((g, seed) -> seed.multiply(l));
186    seedsM.replaceAll((m, seed) -> seed.multiply(l));
187  }
188
189  public final void rotate(final double angle) {
190    synchronized (seedsG) {
191      seedsG.replaceAll((__, seed) -> Points.rotate(seed, angle));
192//      for (Entry<G, Point3D> seed : seedsG.entrySet())
193//        seedsG.put(seed.getKey(), Points.rotate(seed.getValue(), angle));
194    }
195    synchronized (seedsM) {
196      seedsM.replaceAll((__, seed) -> Points.rotate(seed, angle));
197//      for (Entry<M, Point3D> seed : seedsM.entrySet())
198//        seedsM.put(seed.getKey(), Points.rotate(seed.getValue(), angle));
199    }
200    invalidate();
201  }
202
203  @SuppressWarnings("incomplete-switch")
204  public final void move(final Concept<G, M> concept, final ConceptMovement movement, final Point3D delta) {
205    synchronized (seedsG) {
206      synchronized (seedsM) {
207        final double dx = delta.getX();
208        final double dy = delta.getY();
209        final double dz = delta.getZ();
210        switch (movement) {
211        case LABEL_SEED:
212          try {
213            final M affectedSeed = Iterators
214                .getOnlyElement(Sets.intersection(lattice.attributeLabels(concept), seedsM.keySet()).iterator());
215            final Point3D seed = seedsM.get(affectedSeed);
216            seedsM
217                .put(affectedSeed, new Point3D(seed.getX() + dx, Math.max(0.001d, seed.getY() + dy), seed.getZ() + dz));
218          } catch (NoSuchElementException | IllegalArgumentException e) {
219            System.err.println(
220                e.getStackTrace()[0] + " Moving only label seeds, but there was none or more than one! Check this.");
221            System.err.println("\t" + Sets.intersection(lattice.attributeLabels(concept), seedsM.keySet()));
222          }
223          break;
224        case LABEL_CHAIN_SEEDS:
225          try {
226            final M m = Iterators
227                .getOnlyElement(Sets.intersection(lattice.attributeLabels(concept), seedsM.keySet()).iterator());
228            final Point3D s = seedsM.get(m);
229            final HashSet<M> eq = new HashSet<M>(Maps.filterValues(seedsM, new Predicate<Point3D>() {
230
231              public final boolean apply(final Point3D p) {
232                return s.equals(p);// || s.distance(p) < s.distance(0, 0, 0) / 16d;
233              }
234            }).keySet());
235            final double f = 1d / (double) Sets.intersection(eq, concept.intent()).size();
236            final Point3D t = new Point3D(s.getX() + f * dx, Math.max(0.1d, s.getY() + f * dy), s.getZ() + f * dz);
237            for (M n : eq)
238              seedsM.put(n, t);
239          } catch (NoSuchElementException | IllegalArgumentException e) {
240            System.err.println(
241                e.getStackTrace()[0] + " Moving only label seeds, but there was none or more than one! Check this.");
242            System.err.println("\t" + Sets.intersection(lattice.attributeLabels(concept), seedsM.keySet()));
243          }
244          break;
245        case INTENT_SEEDS:
246        case INTENT_CHAIN_SEEDS:
247          final Set<M> affectedSeedsM = new HashSet<M>(Sets.intersection(seedsM.keySet(), concept.intent()));
248          final Set<G> affectedSeedsG = new HashSet<G>(Sets.difference(seedsG.keySet(), concept.extent()));
249          final Point3D a = Points.absoluteSum(Collections2.transform(affectedSeedsG, seedsG::get)).add(
250              Points.absoluteSum(Collections2.transform(affectedSeedsM, seedsM::get)));
251          switch (movement) {
252          case INTENT_SEEDS:
253            if (type.getValue().equals(AdditiveConceptLayout.Type.HYBRID)
254                || type.getValue().equals(AdditiveConceptLayout.Type.OBJECT))
255              for (G g : affectedSeedsG) {
256                final Point3D s = seedsG.get(g);
257                final double fx = a.getX() == 0 ? 1d / (double) affectedSeedsG.size() : Math.abs(s.getX()) / a.getX();
258                final double fy = a.getY() == 0 ? 1d / (double) affectedSeedsG.size() : Math.abs(s.getY()) / a.getY();
259                final double fz = a.getZ() == 0 ? 1d / (double) affectedSeedsG.size() : Math.abs(s.getZ()) / a.getZ();
260                final Point3D t =
261                    new Point3D(s.getX() + fx * dx, Math.max(0.1d, s.getY() + fy * dy), s.getZ() + fz * dz);
262                seedsG.put(g, t);
263              }
264            if (type.getValue().equals(AdditiveConceptLayout.Type.HYBRID)
265                || type.getValue().equals(AdditiveConceptLayout.Type.ATTRIBUTE))
266              for (M m : affectedSeedsM) {
267                final Point3D s = seedsM.get(m);
268                final double fx = a.getX() == 0 ? 1d / (double) affectedSeedsM.size() : Math.abs(s.getX()) / a.getX();
269                final double fy = a.getY() == 0 ? 1d / (double) affectedSeedsM.size() : Math.abs(s.getY()) / a.getY();
270                final double fz = a.getZ() == 0 ? 1d / (double) affectedSeedsM.size() : Math.abs(s.getZ()) / a.getZ();
271                final Point3D t =
272                    new Point3D(s.getX() + fx * dx, Math.max(0.1d, s.getY() + fy * dy), s.getZ() + fz * dz);
273                seedsM.put(m, t);
274              }
275            break;
276          case INTENT_CHAIN_SEEDS:
277            final double size;
278            switch (type.getValue()) {
279            case HYBRID:
280              size = affectedSeedsG.size() + affectedSeedsM.size();
281              break;
282            case OBJECT:
283              size = affectedSeedsG.size();
284              break;
285            case ATTRIBUTE:
286              size = affectedSeedsM.size();
287              break;
288            default:
289              size = 1d;
290              break;
291            }
292
293            if (type.getValue().equals(AdditiveConceptLayout.Type.HYBRID)
294                || type.getValue().equals(AdditiveConceptLayout.Type.OBJECT))
295              while (!affectedSeedsG.isEmpty()) {
296                final G g = Iterables.getFirst(affectedSeedsG, null);
297                final Point3D s = seedsG.get(g);
298                final HashSet<G> eq = new HashSet<G>(Maps.filterValues(seedsG, new Predicate<Point3D>() {
299
300                  public final boolean apply(final Point3D p) {
301                    return s.equals(p);// || s.distance(p) < s.distance(0, 0, 0) / 16d;
302                  }
303                }).keySet());
304                affectedSeedsG.removeAll(eq);
305                final double fx = a.getX() == 0 ? 1d / size : Math.abs(s.getX()) / a.getX();
306                final double fy = a.getY() == 0 ? 1d / size : Math.abs(s.getY()) / a.getY();
307                final double fz = a.getZ() == 0 ? 1d / size : Math.abs(s.getZ()) / a.getZ();
308                final Point3D t =
309                    new Point3D(s.getX() + fx * dx, Math.max(0.1d, s.getY() + fy * dy), s.getZ() + fz * dz);
310                for (G h : eq)
311                  seedsG.put(h, t);
312              }
313            if (type.getValue().equals(AdditiveConceptLayout.Type.HYBRID)
314                || type.getValue().equals(AdditiveConceptLayout.Type.ATTRIBUTE))
315              while (!affectedSeedsM.isEmpty()) {
316                final M m = Iterables.getFirst(affectedSeedsM, null);
317                final Point3D s = seedsM.get(m);
318                final HashSet<M> eq = new HashSet<M>(Maps.filterValues(seedsM, new Predicate<Point3D>() {
319
320                  public final boolean apply(final Point3D p) {
321                    return s.equals(p);// || s.distance(p) < s.distance(0, 0, 0) / 16d;
322                  }
323                }).keySet());
324                affectedSeedsM.removeAll(eq);
325                final double fx = a.getX() == 0 ? 1d / size : Math.abs(s.getX()) / a.getX();
326                final double fy = a.getY() == 0 ? 1d / size : Math.abs(s.getY()) / a.getY();
327                final double fz = a.getZ() == 0 ? 1d / size : Math.abs(s.getZ()) / a.getZ();
328                final Point3D t =
329                    new Point3D(s.getX() + fx * dx, Math.max(0.1d, s.getY() + fy * dy), s.getZ() + fz * dz);
330                for (M n : eq)
331                  seedsM.put(n, t);
332              }
333            break;
334          }
335          break;
336        }
337      }
338    }
339    invalidate();
340  }
341
342  public final void deleteZ() {
343    synchronized (seedsG) {
344      seedsG.replaceAll((g, seed) -> seed.subtract(0, 0, seed.getZ()));
345    }
346    synchronized (seedsM) {
347//      seedsM.putAll(new HashMap<M, Point3D>(Maps.transformValues(seedsM, Points.XY_PROJECTION)));
348      seedsM.replaceAll((m, seed) -> seed.subtract(0, 0, seed.getZ()));
349    }
350    invalidate();
351  }
352
353  public final AdditiveConceptLayout<G, M> clone() {
354    return new AdditiveConceptLayout<G, M>(lattice, seedsG, seedsM, type.getValue());
355  }
356
357  public final boolean equals(final Object o) {
358    return o != null && o instanceof AdditiveConceptLayout && ((AdditiveConceptLayout<?, ?>) o).lattice.equals(lattice)
359        && ((AdditiveConceptLayout<?, ?>) o).seedsG.equals(seedsG)
360        && ((AdditiveConceptLayout<?, ?>) o).seedsM.equals(seedsM);
361  }
362
363  public final int hashCode() {
364    return 7 * lattice.hashCode() + 13 * seedsG.hashCode() + 23 * seedsM.hashCode();
365  }
366}