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}