001package conexp.fx.gui.graph; 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.Arrays; 026import java.util.Collection; 027import java.util.HashMap; 028import java.util.Map; 029import java.util.Random; 030import java.util.Set; 031import java.util.stream.Collectors; 032 033import org.ujmp.core.util.RandomSimple; 034 035import com.google.common.collect.Collections2; 036import com.google.common.collect.Iterables; 037 038import conexp.fx.core.context.Concept; 039import conexp.fx.core.context.ConceptLattice; 040import conexp.fx.core.layout.ChainDecomposer; 041import conexp.fx.core.math.Math3; 042import javafx.geometry.Point2D; 043import javafx.scene.Scene; 044import javafx.scene.layout.AnchorPane; 045import javafx.scene.paint.Color; 046import javafx.scene.shape.Arc; 047import javafx.scene.shape.Circle; 048import javafx.stage.Stage; 049 050public class PolarGraph<G, M> { 051 052 private final Stage primaryStage; 053 private final AnchorPane rootPane; 054 055 public PolarGraph(final ConceptLattice<G, M> lattice) { 056 super(); 057 this.primaryStage = new Stage(); 058 this.rootPane = new AnchorPane(); 059 this.rootPane.setCenterShape(true); 060 this.primaryStage.setScene(new Scene(rootPane, 1280, 800)); 061 this.initialize(lattice); 062 } 063 064 private void initialize(final ConceptLattice<G, M> lattice) { 065 final ChainDecomposer<Set<Integer>> chainDecomposer = new ChainDecomposer<Set<Integer>>( 066 lattice.context.selection._reduced.clone().attributeQuasiOrder().neighborhood()); 067 final Random rng = new RandomSimple(); 068 final Map<M, Point2D> seeds = new HashMap<M, Point2D>(); 069 final Map<Concept<G, M>, Point2D> positions = new HashMap<Concept<G, M>, Point2D>(); 070 final Set<Set<Set<Integer>>> chains = chainDecomposer.randomChainDecomposition(); 071 final int w = chains.size(); 072 if (w == 1) { 073 final Point2D seed = new Point2D(1d, 0d); 074 for (M m : Collections2.transform(Iterables.getOnlyElement(chains), lattice.context.selection._firstAttribute)) 075 seeds.put(m, seed); 076 } else { 077 int i = 0; 078 for (Set<Set<Integer>> chain : chains) { 079 final Point2D seed = new Point2D(1d, ((double) i) / ((double) w) * 2d * Math.PI); 080 for (M m : Collections2.transform(chain, lattice.context.selection._firstAttribute)) 081 seeds.put(m, seed); 082 i++; 083 } 084 } 085 for (Concept<G, M> concept : lattice.rowHeads()) 086 if (!concept.equals(lattice.context.bottomConcept())) { 087 positions.put( 088 concept, 089 PolarPoints.polarSum( 090 seeds 091 .entrySet() 092 .stream() 093 .filter(e -> concept.intent().contains(e.getKey())) 094 .map(e -> e.getValue()) 095 .collect(Collectors.toList()))); 096 } 097 positions.values().forEach(this::addCircleAt); 098 for (Concept<G, M> c1 : lattice.rowHeads()) 099 if (!c1.equals(lattice.context.bottomConcept())) 100 for (Concept<G, M> c2 : lattice.col(c1)) 101 if (!c2.equals(lattice.context.bottomConcept())) 102 drawLine(positions.get(c1), positions.get(c2)); 103 } 104 105 private final void drawLine(final Point2D p, final Point2D q) { 106 final Point2D _p = transform(p); 107 final Point2D _q = transform(q); 108 final Arc e = new Arc( 109 rootPane.getWidth() / 2d, 110 rootPane.getHeight() / 2d, 111 100d * p.getX(), 112 100d * q.getX(), 113 p.getY(), 114 q.getY() - p.getY()); 115 116 rootPane.getChildren().add(e); 117// rootPane.getChildren().add(new Line(_p.getX(), _p.getY(), _q.getX(), _q.getY())); 118 } 119 120 private final void addCircleAt(final Point2D p) { 121 final Point2D q = transform(p); 122 rootPane.getChildren().add(new Circle(q.getX(), q.getY(), 10d, Color.CHARTREUSE)); 123 } 124 125 private Point2D transform(final Point2D p) { 126 final Point2D q = PolarPoints.toCartesian(p); 127 return new Point2D(rootPane.getWidth() / 2d + 100d * q.getX(), rootPane.getHeight() / 2d + 100d * q.getY()); 128 } 129 130// private final Point2D getPolarSum(Stream<Point2D> points) { 131// final DoubleAdder radius = new DoubleAdder(); 132// final DoubleAdder angle = new DoubleAdder(); 133// final LongAdder count = new LongAdder(); 134// points.forEach(p -> { 135// radius.add(p.getX()); 136// angle.add(p.getY()); 137// count.increment(); 138// }); 139// return new Point2D(radius.doubleValue(), angle.doubleValue() / count.doubleValue()); 140// } 141 142 public static final class PolarPoints { 143 144 public static final double angleDistance(final double a, final double b) { 145 return Math.min(Math3.modulo(a - b, 360d), Math3.modulo(b - a, 360d)); 146 } 147 148 public static final double averageAngle(final double a, final double b) { 149 if (a == b) 150 return a; 151 final double _a = Math3.modulo(a, 360d); 152 final double _b = Math3.modulo(b, 360d); 153 if (_a == _b) 154 return _a; 155 final double d = angleDistance(_a, _b) / 2d; 156 if (Math3.modulo(_b - _a, 360d) < Math3.modulo(_a - _b, 360d)) 157 return Math3.modulo(_a + d, 360d); 158 else 159 return Math3.modulo(_b + d, 360d); 160 } 161 162 public static final Point2D polarSum(final Point2D... ps) { 163 return polarSum(Arrays.asList(ps)); 164 } 165 166 public static final Point2D polarSum(final Collection<Point2D> ps) { 167 final double r = ps.stream().map(p -> p.getX()).reduce(0d, Double::sum); 168 final double a = 169 toPolar(ps.stream().map(PolarPoints::toCartesian).reduce(new Point2D(0, 0), (p, q) -> p.add(q))).getY(); 170 return new Point2D(r, a); 171 } 172 173 public static final Point2D toPolar(final Point2D q) { 174 final double r = Math.sqrt(q.getX() * q.getX() + q.getY() * q.getY()); 175 final double a = Math.atan2(q.getX(), q.getY()); 176 return new Point2D(r, a); 177 } 178 179 public static final Point2D toCartesian(final Point2D p) { 180 final double x = p.getX() * Math.sin(p.getY()); 181 final double y = p.getX() * Math.cos(p.getY()); 182 return new Point2D(x, y); 183 } 184 } 185 186 public final void show() { 187 this.primaryStage.show(); 188 } 189 190// public static final void main(String[] args) { 191//// System.out.println(angleDistance(300, 60)); 192//// System.out.println(angleDistance(60, 300)); 193//// System.out.println(angleDistance(300 + 360, 60 + 720)); 194//// System.out.println(angleDistance(60 - 720, 300)); 195//// System.out.println(averageAngle(300, 60)); 196//// System.out.println(averageAngle(60, 300)); 197//// System.out.println(averageAngle(300 + 360, 60 + 720)); 198//// System.out.println(averageAngle(60 - 720, 300)); 199//// System.out.println(averageAngle(0, 10)); 200//// System.out.println(averageAngle(10, 0)); 201//// System.out.println(averageAngle(0, 350)); 202//// System.out.println(averageAngle(350, 0)); 203//// System.out.println(averageAngle(180, 270)); 204// } 205}