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}