001package conexp.fx.gui.context;
002
003/*
004 * #%L
005 * Concept Explorer FX
006 * %%
007 * Copyright (C) 2010 - 2019 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 */
024import java.util.Collection;
025import java.util.Map;
026import java.util.Map.Entry;
027import java.util.Timer;
028import java.util.TimerTask;
029import java.util.concurrent.ConcurrentHashMap;
030
031import org.semanticweb.owlapi.model.OWLClassExpression;
032
033import com.google.common.base.Function;
034import com.google.common.collect.Collections2;
035
036import conexp.fx.core.collections.Pair;
037import conexp.fx.core.collections.relation.RelationEvent;
038import conexp.fx.core.collections.relation.RelationEventHandler;
039import conexp.fx.core.context.Concept;
040import conexp.fx.core.context.MatrixContext;
041import conexp.fx.core.context.MatrixContext.Incidence;
042import conexp.fx.core.util.Constants;
043import conexp.fx.core.util.IdGenerator;
044import conexp.fx.core.util.OWLUtil;
045import conexp.fx.gui.ConExpFX;
046import conexp.fx.gui.cellpane.Cell;
047import conexp.fx.gui.cellpane.CellPane;
048import conexp.fx.gui.cellpane.InteractionMode;
049import conexp.fx.gui.dataset.FCADataset;
050import conexp.fx.gui.graph.ConceptGraph;
051import conexp.fx.gui.util.ColorScheme;
052import conexp.fx.gui.util.LaTeX;
053import conexp.fx.gui.util.Platform2;
054import de.tudresden.inf.tcs.fcalib.Implication;
055import javafx.animation.Interpolator;
056import javafx.animation.KeyFrame;
057import javafx.animation.KeyValue;
058import javafx.animation.Timeline;
059import javafx.application.Platform;
060import javafx.beans.binding.DoubleBinding;
061import javafx.beans.binding.IntegerBinding;
062import javafx.beans.binding.StringBinding;
063import javafx.beans.property.BooleanProperty;
064import javafx.beans.property.DoubleProperty;
065import javafx.beans.property.IntegerProperty;
066import javafx.beans.property.SimpleBooleanProperty;
067import javafx.beans.property.SimpleDoubleProperty;
068import javafx.beans.property.SimpleIntegerProperty;
069import javafx.beans.value.ChangeListener;
070import javafx.beans.value.ObservableValue;
071import javafx.collections.MapChangeListener;
072import javafx.event.ActionEvent;
073import javafx.event.EventHandler;
074import javafx.geometry.Bounds;
075import javafx.geometry.Insets;
076import javafx.geometry.Pos;
077import javafx.scene.Node;
078import javafx.scene.control.Button;
079import javafx.scene.control.ButtonBuilder;
080import javafx.scene.control.ContextMenu;
081import javafx.scene.control.MenuItem;
082import javafx.scene.control.ScrollBar;
083import javafx.scene.control.Slider;
084import javafx.scene.control.SliderBuilder;
085import javafx.scene.control.TextField;
086import javafx.scene.control.TextFieldBuilder;
087import javafx.scene.control.ToggleButton;
088import javafx.scene.control.ToolBar;
089import javafx.scene.image.Image;
090import javafx.scene.image.ImageView;
091import javafx.scene.image.ImageViewBuilder;
092import javafx.scene.input.KeyEvent;
093import javafx.scene.input.MouseEvent;
094import javafx.scene.layout.BorderPane;
095import javafx.scene.layout.ColumnConstraints;
096import javafx.scene.layout.GridPane;
097import javafx.scene.layout.HBox;
098import javafx.scene.layout.RowConstraints;
099import javafx.scene.paint.Color;
100import javafx.scene.text.TextAlignment;
101import javafx.util.Duration;
102
103public class MatrixContextWidget<G, M> extends BorderPane {
104
105  public final class RowHeaderPane extends CellPane<RowHeaderPane, RowHeaderCell> {
106
107    private RowHeaderPane(final boolean interactive) {
108      super("DomainPane", InteractionMode.ROWS, interactive);
109      this.rowHeightDefault.bind(MatrixContextWidget.this.cellSizeDefault);
110      this.columnWidthDefault.bind(MatrixContextWidget.this.rowHeaderSizeDefault);
111      this.zoomFactor.bind(MatrixContextWidget.this.zoomFactor);
112      this.textSizeDefault.bind(MatrixContextWidget.this.textSizeDefault);
113      this.animate.bind(MatrixContextWidget.this.animate);
114      this.autoSizeRows.set(false);
115      this.autoSizeColumns.set(true);
116      this.maxColumns.set(1);
117      this.maxRows.set(context.rowHeads().size());
118//      final RelationEventHandler<G, M> eventHandler = new RelationEventHandler<G, M>() {
119//
120//        public final void handle(final RelationEvent<G, M> event) {
121//          Platform2.runOnFXThread(new Runnable() {
122//
123//            public void run() {
124////              System.out.println("updating rows");
125//              maxRows.set(context.rowHeads().size());
126////              updateContent();
127//            }
128//          });
129//        }
130//      };
131//      context.addEventHandler(eventHandler, RelationEvent.ROWS);
132//      context.addEventHandler(eventHandler, RelationEvent.ROWS_ADDED);
133//      context.addEventHandler(eventHandler, RelationEvent.ROWS_REMOVED);
134      context.addEventHandler(
135          __ -> Platform2.runOnFXThread(() -> maxRows.set(context.rowHeads().size())),
136          RelationEvent.ROWS);
137      if (dataset != null)
138        this.interactionPane.addEventHandler(MouseEvent.MOUSE_EXITED, new EventHandler<MouseEvent>() {
139
140          public final void handle(final MouseEvent event) {
141            dataset.conceptGraph.highlight(false, dataset.conceptGraph.highlightRequests.dehighlight());
142          }
143        });
144      zoomFactor.addListener((__, ___, ____) -> updateContent());
145    }
146
147    protected final RowHeaderCell createCell(final int row, final int column) {
148      return new RowHeaderCell(row);
149    }
150
151    public final void highlightConcept(final Collection<Integer> domainIndices) {
152      highlight(domainIndices, null);
153    }
154
155    protected final Map<G, Node> decorations = new ConcurrentHashMap<>();
156
157    public final void addDecoration(final G row, final Node decoration) {
158      decorations.put(row, decoration);
159    }
160  }
161
162  public final class ColHeaderPane extends CellPane<ColHeaderPane, ColHeaderCell> {
163
164    private ColHeaderPane(final boolean interactive) {
165      super("CodomainPane", InteractionMode.COLUMNS, interactive);
166      this.rowHeightDefault.bind(MatrixContextWidget.this.colHeaderSizeDefault);
167      this.columnWidthDefault.bind(MatrixContextWidget.this.cellSizeDefault);
168      this.zoomFactor.bind(MatrixContextWidget.this.zoomFactor);
169      this.textSizeDefault.bind(MatrixContextWidget.this.textSizeDefault);
170      this.animate.bind(MatrixContextWidget.this.animate);
171      this.autoSizeRows.set(true);
172      this.autoSizeColumns.set(false);
173      this.maxRows.set(1);
174      this.maxColumns.set(context.colHeads().size());
175      final RelationEventHandler<G, M> eventHandler = new RelationEventHandler<G, M>() {
176
177        public final void handle(final RelationEvent<G, M> event) {
178          Platform2.runOnFXThread(new Runnable() {
179
180            public void run() {
181//              System.out.println("updating columns");
182              maxColumns.set(context.colHeads().size());
183//              updateContent();
184            }
185          });
186        }
187      };
188      context.addEventHandler(eventHandler, RelationEvent.COLUMNS);
189//      context.addEventHandler(eventHandler, RelationEvent.COLUMNS_ADDED);
190//      context.addEventHandler(eventHandler, RelationEvent.COLUMNS_REMOVED);
191      if (dataset != null)
192        this.interactionPane.addEventHandler(MouseEvent.MOUSE_EXITED, new EventHandler<MouseEvent>() {
193
194          public final void handle(final MouseEvent event) {
195            dataset.conceptGraph.highlight(false, dataset.conceptGraph.highlightRequests.dehighlight());
196          }
197        });
198      zoomFactor.addListener((__, ___, ____) -> updateContent());
199    }
200
201    protected final ColHeaderCell createCell(final int row, final int column) {
202      return new ColHeaderCell(column);
203    }
204
205    public final void highlightConcept(final Collection<Integer> codomainIndices) {
206      highlight(null, codomainIndices);
207    }
208  }
209
210  public final class ContextPane extends CellPane<ContextPane, ContextCell> {
211
212    private ContextPane(final boolean interactive) {
213      super("FormalContextPane", InteractionMode.ROWS_AND_COLUMNS, interactive);
214      this.textSizeDefault.bind(MatrixContextWidget.this.incidenceSizeDefault);
215      this.zoomFactor.bind(MatrixContextWidget.this.zoomFactor);
216      this.bind(rowHeaderPane, InteractionMode.ROWS);
217      this.bind(colHeaderPane, InteractionMode.COLUMNS);
218      if (dataset != null)
219        this.rowMap.addListener(new MapChangeListener<Integer, Integer>() {
220
221          public final void onChanged(
222              final javafx.collections.MapChangeListener.Change<? extends Integer, ? extends Integer> change) {
223            dataset.unsavedChanges.set(true);
224          }
225        });
226      if (dataset != null)
227        this.columnMap.addListener(new MapChangeListener<Integer, Integer>() {
228
229          public final void onChanged(
230              final javafx.collections.MapChangeListener.Change<? extends Integer, ? extends Integer> change) {
231            dataset.unsavedChanges.set(true);
232          }
233        });
234      if (dataset != null)
235        this.interactionPane.addEventHandler(MouseEvent.MOUSE_EXITED, new EventHandler<MouseEvent>() {
236
237          public final void handle(final MouseEvent event) {
238            dataset.conceptGraph.highlight(false, dataset.conceptGraph.highlightRequests.dehighlight());
239          }
240        });
241      zoomFactor.addListener((__, ___, ____) -> updateContent());
242    }
243
244    protected final ContextCell createCell(final int row, final int column) {
245      return new ContextCell(row, column);
246    }
247  }
248
249  public final class RowHeaderCell extends Cell<RowHeaderCell, RowHeaderPane> {
250
251    private ImageView view = ImageViewBuilder.create().build();
252
253    private RowHeaderCell(final int row) {
254      super(rowHeaderPane, row, 0, Pos.CENTER_RIGHT, TextAlignment.RIGHT, false, null, false);
255      this.contentPane.get().getChildren().add(view);
256      this.contentPane.get().text.setOpacity(0);
257      if (cellPane.interactive) {
258        final ContextMenu contextMenu = new ContextMenu();
259        final MenuItem editItem = new MenuItem("Edit");
260        final MenuItem removeItem = new MenuItem("Remove");
261        final MenuItem selectItem = new MenuItem("Select");
262        final MenuItem insertItem = new MenuItem("Insert");
263        if (dataset != null && dataset.editable)
264          contextMenu.getItems().addAll(editItem, removeItem, selectItem, insertItem);
265        else
266          contextMenu.getItems().addAll(removeItem, selectItem);
267        insertItem.setOnAction(__ -> {
268          ((FCADataset<String, String>) dataset).addObject(
269              (dataset.context.isHomogen() ? "Element " : "Object ") + IdGenerator.getNextId(dataset),
270              contentCoordinates.get().x());
271          rowHeaderPane.rowOpacityMap.keySet().stream().sorted().filter(i -> i >= contentCoordinates.get().x()).forEach(
272              i -> rowHeaderPane.rowOpacityMap.put(i + 1, rowHeaderPane.rowOpacityMap.remove(i)));
273        });
274        editItem.setOnAction(__ -> {
275          if (MatrixContextWidget.this.dataset.editable) {
276            final G object = context.rowHeads().get(contentCoordinates.get().x());
277            final TextField textField = TextFieldBuilder.create().text((String) object).build();
278            textField.addEventHandler(KeyEvent.KEY_RELEASED, event -> {
279              switch (event.getCode()) {
280              case ENTER:
281                dataset.renameObject(object, (G) textField.getText().trim());
282              case ESCAPE:
283//                interactionPane.get().getChildren().remove(textField);
284                contentPane.get().getChildren().remove(textField);
285                contentPane.get().getChildren().add(view);
286              }
287            });
288//            interactionPane.get().getChildren().add(textField);
289            contentPane.get().getChildren().remove(view);
290            contentPane.get().getChildren().add(textField);
291            textField.focusedProperty().addListener(
292                (observable, oldValue, newValue) -> new Timer().schedule(new TimerTask() {
293
294              public final void run() {
295                Platform.runLater(() -> textField.selectAll());
296              }
297            }, 20));
298            textField.requestFocus();
299          } else {
300            System.out.println("no instance of MatrixContextWidget");
301          }
302        });
303        removeItem.setOnAction(__ -> {
304          dataset.removeObject(context.rowHeads().get(contentCoordinates.get().x()));
305          rowHeaderPane.rowOpacityMap.keySet().stream().sorted().filter(i -> i > contentCoordinates.get().x()).forEach(
306              i -> rowHeaderPane.rowOpacityMap.put(i - 1, rowHeaderPane.rowOpacityMap.remove(i)));
307        });
308        selectItem.setOnAction(__ -> select());
309        this.interactionPane.get().addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
310          switch (event.getButton()) {
311          case PRIMARY:
312            select();
313            break;
314          case SECONDARY:
315            contextMenu.show(interactionPane.getValue(), event.getScreenX(), event.getScreenY());
316          }
317        });
318      }
319      if (dataset != null)
320        this.interactionPane.get().addEventHandler(MouseEvent.MOUSE_ENTERED, event -> {
321          if (contextPane.highlight.get())
322            dataset.conceptGraph.highlight(
323                true,
324                dataset.conceptGraph.highlightRequests.object(context.rowHeads().get(contentCoordinates.get().x())));
325        });
326      if (cellPane.autoSizeRows.get() || cellPane.autoSizeColumns.get())
327        view.layoutBoundsProperty().addListener(new ChangeListener<Bounds>() {
328
329          @Override
330          public void changed(ObservableValue<? extends Bounds> observable, Bounds oldValue, Bounds newValue) {
331            final double decorationWidth =
332                cellPane.decorations.containsKey(context.rowHeads().get(contentCoordinates.get().x()))
333                    ? cellPane.decorations
334                        .get(context.rowHeads().get(contentCoordinates.get().x()))
335                        .layoutBoundsProperty()
336                        .get()
337                        .getWidth()
338                    : 0d;
339            final double width = newValue.getWidth() + decorationWidth;
340            if (width > cellPane.maximalTextWidth.get())
341              cellPane.maximalTextWidth.set(width);
342          }
343        });
344      context.addEventHandler(event -> Platform2.runOnFXThread(RowHeaderCell.this::updateContent), RelationEvent.ROWS);
345      updateContent();
346    }
347
348    private final void select() {
349      synchronized (rowHeaderPane.rowOpacityMap) {
350        if (rowHeaderPane.rowOpacityMap.containsKey(contentCoordinates.get().x())) {
351          if (dataset != null)
352            dataset.conceptGraph.highlight(false, dataset.conceptGraph.highlightRequests.dehighlight());
353          rowHeaderPane.rowOpacityMap.remove(contentCoordinates.get().x());
354          if (dataset != null)
355            dataset.selectObject(context.rowHeads().get(contentCoordinates.get().x()));
356        } else {
357          if (dataset != null)
358            dataset.conceptGraph.highlight(false, dataset.conceptGraph.highlightRequests.dehighlight());
359          rowHeaderPane.rowOpacityMap.put(contentCoordinates.get().x(), Constants.HIDE_OPACITY);
360          if (dataset != null)
361            dataset.ignoreObject(context.rowHeads().get(contentCoordinates.get().x()));
362        }
363      }
364//    if (rowHeaderPane.rowOpacityMap.containsKey(contentCoordinates.get().x())) {
365//    rowHeaderPane.rowOpacityMap.remove(contentCoordinates.get().x());
366////  final G object = context.getDomain().get(contentCoordinates.get().x());
367////  context.selectObject(object);
368//  } else {
369//    rowHeaderPane.rowOpacityMap.put(contentCoordinates.get().x(), Constants.HIDE_OPACITY);
370////  final G object = context.getDomain().get(contentCoordinates.get().x());
371////  context.deselectObject(object);
372//  }
373    }
374
375    public final void updateContent() {
376      try {
377        final String string = context.rowHeads().get(contentCoordinates.get().x()).toString();
378        textContent.set(string);
379        view.setImage(LaTeX.toFXImage(string, (float) (16d * zoomFactor.get())));
380        if (cellPane.decorations.containsKey(context.rowHeads().get(contentCoordinates.get().x()))) {
381          interactionPane.get().getChildren().removeAll(cellPane.decorations.values());
382          interactionPane
383              .get()
384              .setRight(cellPane.decorations.get(context.rowHeads().get(contentCoordinates.get().x())));
385        }
386      } catch (IndexOutOfBoundsException __) {}
387    }
388  }
389
390  public final class ColHeaderCell extends Cell<ColHeaderCell, ColHeaderPane> {
391
392    private ImageView view = ImageViewBuilder.create().build();
393
394    private ColHeaderCell(final int column) {
395      super(colHeaderPane, 0, column, Pos.CENTER_LEFT, TextAlignment.LEFT, true, null, false);
396      this.contentPane.get().getChildren().add(view);
397      this.contentPane.get().text.setOpacity(0);
398      if (cellPane.interactive) {
399        final ContextMenu contextMenu = new ContextMenu();
400        final MenuItem editItem = new MenuItem("Edit");
401        final MenuItem removeItem = new MenuItem("Remove");
402        final MenuItem selectItem = new MenuItem("Select");
403        final MenuItem insertItem = new MenuItem("Insert");
404        if (dataset != null && dataset.editable)
405          contextMenu.getItems().addAll(editItem, removeItem, selectItem, insertItem);
406        else
407          contextMenu.getItems().addAll(removeItem, selectItem);
408        insertItem.setOnAction(__ -> {
409          ((FCADataset<String, String>) dataset).addAttribute(
410              (dataset.context.isHomogen() ? "Element " : "Attribute ") + IdGenerator.getNextId(dataset),
411              contentCoordinates.get().y());
412          colHeaderPane.columnOpacityMap
413              .keySet()
414              .stream()
415              .sorted()
416              .filter(i -> i >= contentCoordinates.get().y())
417              .forEach(i -> colHeaderPane.columnOpacityMap.put(i + 1, colHeaderPane.columnOpacityMap.remove(i)));
418        });
419        editItem.setOnAction(event -> {
420          if (MatrixContextWidget.this.dataset.editable) {
421            final M attribute = context.colHeads().get(contentCoordinates.get().y());
422            final TextField textField = TextFieldBuilder.create().text((String) attribute).build();
423            textField.addEventHandler(KeyEvent.KEY_RELEASED, keyEvent -> {
424              switch (keyEvent.getCode()) {
425              case ENTER:
426                dataset.renameAttribute(attribute, (M) textField.getText().trim());
427              case ESCAPE:
428//                interactionPane.get().getChildren().remove(textField);
429                contentPane.get().getChildren().remove(textField);
430                contentPane.get().getChildren().add(view);
431              }
432            });
433//            textField.rotateProperty().set(-90);
434            textField.setMinSize(colHeaderPane.rowHeight.get(), cellSize.get());
435            textField.setMaxSize(colHeaderPane.rowHeight.get(), cellSize.get());
436//            interactionPane.get().getChildren().add(textField);
437            contentPane.get().getChildren().remove(view);
438            contentPane.get().getChildren().add(textField);
439            textField.focusedProperty().addListener(
440                (observable, oldValue, newValue) -> new Timer().schedule(new TimerTask() {
441
442              public final void run() {
443                Platform.runLater(() -> textField.selectAll());
444              }
445            }, 20));
446            textField.requestFocus();
447          }
448        });
449        removeItem.setOnAction(event -> {
450          dataset.removeAttribute(context.colHeads().get(contentCoordinates.get().y()));
451          colHeaderPane.columnOpacityMap.remove(contentCoordinates.get().y());
452          colHeaderPane.columnOpacityMap
453              .keySet()
454              .stream()
455              .sorted()
456              .filter(i -> i > contentCoordinates.get().y())
457              .forEach(i -> colHeaderPane.columnOpacityMap.put(i - 1, colHeaderPane.columnOpacityMap.remove(i)));
458        });
459        selectItem.setOnAction(event -> select());
460//      if (!tab.fca.context.selectedAttributes().contains(tab.fca.context.colHeads().get(contentCoordinates.get().y())))
461//        colHeaderPane.columnOpacityMap.put(contentCoordinates.get().y(), Constants.HIDE_OPACITY);
462        this.interactionPane.get().addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
463          switch (event.getButton()) {
464          case PRIMARY:
465            select();
466            break;
467          case SECONDARY:
468            contextMenu.show(interactionPane.getValue(), event.getScreenX(), event.getScreenY());
469          }
470        });
471      }
472      if (dataset != null)
473        this.interactionPane.get().addEventHandler(MouseEvent.MOUSE_ENTERED, event -> {
474          if (contextPane.highlight.get()) {
475            final M m = context.colHeads().get(contentCoordinates.get().y());
476            if (context.selectedAttributes().contains(m))
477              dataset.conceptGraph.highlight(true, dataset.conceptGraph.highlightRequests.attribute(m));
478          }
479        });
480      if (cellPane.autoSizeRows.get() || cellPane.autoSizeColumns.get())
481        view.layoutBoundsProperty().addListener(new ChangeListener<Bounds>() {
482
483          @Override
484          public void changed(ObservableValue<? extends Bounds> observable, Bounds oldValue, Bounds newValue) {
485            final double width = newValue.getWidth();
486            if (width > cellPane.maximalTextWidth.get())
487              cellPane.maximalTextWidth.set(width);
488          }
489        });
490      context
491          .addEventHandler(event -> Platform2.runOnFXThread(ColHeaderCell.this::updateContent), RelationEvent.COLUMNS);
492      updateContent();
493    }
494
495    private void select() {
496      synchronized (colHeaderPane.columnOpacityMap) {
497        if (colHeaderPane.columnOpacityMap.containsKey(contentCoordinates.get().y())) {
498          if (dataset != null)
499            dataset.conceptGraph.highlight(false, dataset.conceptGraph.highlightRequests.dehighlight());
500          colHeaderPane.columnOpacityMap.remove(contentCoordinates.get().y());
501          if (dataset != null)
502            dataset.selectAttribute(context.colHeads().get(contentCoordinates.get().y()));
503        } else {
504          if (dataset != null)
505            dataset.conceptGraph.highlight(false, dataset.conceptGraph.highlightRequests.dehighlight());
506          colHeaderPane.columnOpacityMap.put(contentCoordinates.get().y(), Constants.HIDE_OPACITY);
507          if (dataset != null)
508            dataset.ignoreAttribute(context.colHeads().get(contentCoordinates.get().y()));
509        }
510      }
511    }
512
513    public final void updateContent() {
514      try {
515        final M m = context.colHeads().get(contentCoordinates.get().y());
516        final String string =
517            m instanceof OWLClassExpression ? OWLUtil.toString((OWLClassExpression) m) : m.toString();
518        textContent.set(string);
519        view.setImage(LaTeX.toFXImage(string, (float) (16d * zoomFactor.get())));
520      } catch (IndexOutOfBoundsException __) {}
521    }
522  }
523
524  public final class ContextCell extends Cell<ContextCell, ContextPane> {
525
526    private ContextCell(final int row, final int column) {
527      super(contextPane, row, column, Pos.CENTER, TextAlignment.CENTER, false, null, false);
528      if (dataset != null)
529        this.interactionPane.get().addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
530
531          public final void handle(final MouseEvent event) {
532            final G g = context.rowHeads().get(contentCoordinates.get().x());
533            final M m = context.colHeads().get(contentCoordinates.get().y());
534            dataset.conceptGraph.highlight(false, dataset.conceptGraph.highlightRequests.dehighlight());
535            dataset.flip(g, m);
536            dataset.unsavedChanges.set(true);
537          }
538        });
539      else
540        this.interactionPane.get().addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
541          final G g = context.rowHeads().get(contentCoordinates.get().x());
542          final M m = context.colHeads().get(contentCoordinates.get().y());
543          if (context.contains(g, m))
544            context.remove(g, m);
545          else
546            context.add(g, m);
547        });
548      if (dataset != null)
549        this.interactionPane.get().addEventHandler(MouseEvent.MOUSE_ENTERED, new EventHandler<MouseEvent>() {
550
551          public final void handle(final MouseEvent event) {
552            if (contextPane.highlight.get()) {
553              final G g = context.rowHeads().get(contentCoordinates.get().x());
554              final M m = context.colHeads().get(contentCoordinates.get().y());
555              if (context.selectedAttributes().contains(m))
556                if (textContent.get().equals(Constants.CROSS_CHARACTER))
557                  dataset.conceptGraph.highlight(true, dataset.conceptGraph.highlightRequests.incidence(g, m));
558                else if (textContent.get().equals(Constants.DOWN_ARROW_CHARACTER))
559                  dataset.conceptGraph.highlight(true, dataset.conceptGraph.highlightRequests.downArrow(g, m));
560                else if (textContent.get().equals(Constants.UP_ARROW_CHARACTER))
561                  dataset.conceptGraph.highlight(true, dataset.conceptGraph.highlightRequests.upArrow(g, m));
562                else if (textContent.get().equals(Constants.BOTH_ARROW_CHARACTER))
563                  dataset.conceptGraph.highlight(true, dataset.conceptGraph.highlightRequests.bothArrow(g, m));
564                else if (textContent.get().equals(Constants.NO_CROSS_CHARACTER))
565                  dataset.conceptGraph.highlight(true, dataset.conceptGraph.highlightRequests.nonIncidence(g, m));
566            }
567          }
568        });
569      context.addEventHandler(event -> updateContent(), RelationEvent.ANY);
570//          RelationEvent.ENTRIES_ADDED,
571//          RelationEvent.ENTRIES_REMOVED,
572//          RelationEvent.ALL_CHANGED,
573//          RelationEvent.SELECTION_CHANGED);
574      updateContent();
575    }
576
577    @SuppressWarnings("incomplete-switch")
578    public final void updateContent() {
579      try {
580        final G g = context.rowHeads().get(contentCoordinates.get().x());
581        final M m = context.colHeads().get(contentCoordinates.get().y());
582        if (context.selectedAttributes().contains(m) && context.selectedObjects().contains(g)) {
583          final Pair<Incidence, Incidence> p = context.selection.getValue(g, m, showArrows.get(), showPaths.get());
584          final Incidence first = p.first();
585          final Incidence second = p.second();
586          ContextCell.this.textContent.set(
587              second != null && first == Incidence.NO_CROSS ? Constants.NO_CROSS_CHARACTER_BOLD : first.toString());
588          switch (first) {
589          case BOTH_ARROW:
590            ContextCell.this.contentPane.get().text.setRotate(-45d);
591            break;
592          case DOWN_ARROW:
593          case UP_ARROW:
594          case CROSS:
595          case NO_CROSS:
596            ContextCell.this.contentPane.get().text.setRotate(0d);
597            break;
598          }
599          if (second == null)
600            ContextCell.this.contentPane.get().text.setFill(Color.BLACK);
601          else
602            switch (second) {
603            case BOTH_PATH:
604              ContextCell.this.contentPane.get().text.setFill(ConceptGraph.COLOR_INTERVAL);
605              break;
606            case DOWN_PATH:
607              ContextCell.this.contentPane.get().text.setFill(ConceptGraph.COLOR_LOWER);
608              break;
609            case UP_PATH:
610              ContextCell.this.contentPane.get().text.setFill(ConceptGraph.COLOR_UPPER);
611              break;
612            }
613        } else {
614          ContextCell.this.textContent.set(context.getValue(g, m, false).first().toString());
615          ContextCell.this.contentPane.get().text.setRotate(0d);
616        }
617      } catch (IndexOutOfBoundsException __) {}
618    }
619  }
620
621  protected final FCADataset<G, M>    dataset;
622  protected final MatrixContext<G, M> context;
623  protected final GridPane            centerPane            = new GridPane();
624  public final RowHeaderPane          rowHeaderPane;
625  public final ColHeaderPane          colHeaderPane;
626  public final ContextPane            contextPane;
627  protected final ScrollBar           rowScrollBar;
628  protected final ScrollBar           colScrollBar;
629//  public final ListSpinner<Integer> zoomSpinner =new ListSpinner<Integer>(-4, 4, 1);
630  public final Slider                 zoomSlider            = SliderBuilder
631                                                                .create()
632                                                                .min(-4d)
633                                                                .max(4d)
634                                                                .value(0d)
635                                                                // .showTickMarks(true)
636                                                                // .minorTickCount(17)
637                                                                // .majorTickUnit(1d)
638                                                                // .snapToTicks(true)
639                                                                .blockIncrement(0.25d)
640                                                                .build();
641  public final DoubleProperty         zoomFactor            = new SimpleDoubleProperty(0.01d);
642  public final IntegerProperty        rowHeaderSizeDefault  = new SimpleIntegerProperty(40);
643  public final IntegerProperty        colHeaderSizeDefault  = new SimpleIntegerProperty(40);
644  public final IntegerProperty        cellSizeDefault       = new SimpleIntegerProperty(20);
645  public final IntegerProperty        textSizeDefault       = new SimpleIntegerProperty(16);
646  public final IntegerProperty        incidenceSizeDefault  = new SimpleIntegerProperty(20);
647  public final IntegerBinding         cellSize              = new IntegerBinding() {
648
649                                                              {
650                                                                super.bind(zoomFactor, cellSizeDefault);
651                                                              }
652
653                                                              protected int computeValue() {
654                                                                return (int) (zoomFactor.get()
655                                                                    * cellSizeDefault.doubleValue());
656                                                              };
657                                                            };
658  public final IntegerBinding         textSize              = new IntegerBinding() {
659
660                                                              {
661                                                                super.bind(zoomFactor, textSizeDefault);
662                                                              }
663
664                                                              protected int computeValue() {
665                                                                return (int) (zoomFactor.get()
666                                                                    * textSizeDefault.doubleValue());
667                                                              };
668                                                            };
669  public final IntegerBinding         incidenceSize         = new IntegerBinding() {
670
671                                                              {
672                                                                super.bind(zoomFactor, textSizeDefault);
673                                                              }
674
675                                                              protected int computeValue() {
676                                                                return (int) (zoomFactor.get()
677                                                                    * incidenceSizeDefault.doubleValue());
678                                                              };
679                                                            };
680  public final BooleanProperty        animate               = new SimpleBooleanProperty(false);
681  public final BooleanProperty        showArrows            = new SimpleBooleanProperty();
682  public final BooleanProperty        showPaths             = new SimpleBooleanProperty();
683  public final ToggleButton           highlightToggleButton = new ToggleButton();
684  public final DoubleBinding          height;
685
686  public MatrixContextWidget(final FCADataset<G, M> fcaInstance) {
687    this(fcaInstance, true);
688  }
689
690  /**
691   * @param dataset
692   * @param withToolbar
693   * @param orContext
694   *          may only be set if fcaInstance is null, otherwise unexpected behaviour may occur.
695   */
696  @SafeVarargs
697  public MatrixContextWidget(
698      final FCADataset<G, M> dataset,
699      final boolean withToolbar,
700      final MatrixContext<G, M>... orContext) {
701    this(dataset, withToolbar, true, orContext);
702  }
703
704  @SafeVarargs
705  public MatrixContextWidget(
706      final FCADataset<G, M> dataset,
707      final boolean withToolbar,
708      final boolean interactive,
709      final MatrixContext<G, M>... orContext) {
710    super();
711    this.dataset = dataset;
712    if (dataset == null && orContext[0] != null)
713      this.context = orContext[0];
714    else
715      this.context = dataset.context;
716    this.rowHeaderPane = new RowHeaderPane(interactive);
717    this.colHeaderPane = new ColHeaderPane(interactive);
718    this.contextPane = new ContextPane(interactive);
719    this.rowScrollBar = contextPane.getRowScrollBar();
720    this.colScrollBar = contextPane.getColumnScrollBar();
721    centerPane.setHgap(4);
722    centerPane.setVgap(4);
723    rowHeaderPane.colorScheme.setValue(ColorScheme.JAVA_FX);
724    colHeaderPane.colorScheme.setValue(ColorScheme.JAVA_FX);
725    contextPane.colorScheme.setValue(ColorScheme.JAVA_FX);
726    final RowConstraints firstRowConstraints = new RowConstraints();
727    firstRowConstraints.minHeightProperty().bind(colHeaderPane.rowHeight);
728    firstRowConstraints.maxHeightProperty().bind(colHeaderPane.rowHeight);
729    final RowConstraints secondRowConstraints = new RowConstraints();
730    secondRowConstraints.minHeightProperty().bind(cellSize);
731    secondRowConstraints.prefHeightProperty().bind(new IntegerBinding() {
732
733      {
734        super.bind(rowHeaderPane.maxRows, cellSize);
735      }
736
737      protected final int computeValue() {
738        return context.rowHeads().size() * cellSize.get();
739      }
740    });
741    final RowConstraints thirdRowConstraints = new RowConstraints();
742    thirdRowConstraints.minHeightProperty().bind(cellSize);
743    thirdRowConstraints.maxHeightProperty().bind(cellSize);
744    centerPane.getRowConstraints().addAll(firstRowConstraints, secondRowConstraints, thirdRowConstraints);
745    final ColumnConstraints firstColumnConstraints = new ColumnConstraints();
746    firstColumnConstraints.minWidthProperty().bind(rowHeaderPane.columnWidth);
747    firstColumnConstraints.maxWidthProperty().bind(rowHeaderPane.columnWidth);
748    final ColumnConstraints secondColumnConstraints = new ColumnConstraints();
749    secondColumnConstraints.minWidthProperty().bind(cellSize);
750    secondColumnConstraints.maxWidthProperty().bind(new IntegerBinding() {
751
752      {
753        super.bind(colHeaderPane.maxColumns, cellSize);
754      }
755
756      protected final int computeValue() {
757        return context.colHeads().size() * cellSize.get();
758      }
759    });
760    final ColumnConstraints thirdColumnConstraints = new ColumnConstraints();
761    thirdColumnConstraints.minWidthProperty().bind(cellSize);
762    thirdColumnConstraints.maxWidthProperty().bind(cellSize);
763    centerPane.getColumnConstraints().addAll(firstColumnConstraints, secondColumnConstraints, thirdColumnConstraints);
764    centerPane.add(contextPane.getContentAndInteractionStackPane(), 1, 1);
765    centerPane.add(rowHeaderPane.getContentAndInteractionStackPane(), 0, 1);
766    centerPane.add(colHeaderPane.getContentAndInteractionStackPane(), 1, 0);
767    centerPane.add(rowScrollBar, 2, 1);
768    centerPane.add(colScrollBar, 1, 2);
769    this.setCenter(centerPane);
770    if (withToolbar)
771      createToolBar();
772    else
773      zoomFactor.set(Math.pow(2d, 0));
774    final ChangeListener<Boolean> updateContentListener = new ChangeListener<Boolean>() {
775
776      public final void
777          changed(final ObservableValue<? extends Boolean> observable, final Boolean oldValue, final Boolean newValue) {
778        contextPane.updateContent();
779      }
780    };
781    showArrows.addListener(updateContentListener);
782    showPaths.addListener(updateContentListener);
783    height = new DoubleBinding() {
784
785      {
786        bind(
787            colHeaderPane.heightProperty(),
788            contextPane.heightProperty(),
789            colHeaderPane.rowHeight,
790            colHeaderPane.visibleRows,
791            contextPane.rowHeight,
792            contextPane.visibleRows);
793      }
794
795      @Override
796      protected double computeValue() {
797//        System.out.println(colHeaderPane.rowHeight.get());
798//        System.out.println(colHeaderPane.visibleRows.get());
799//        System.out.println(contextPane.rowHeight.get());
800//        System.out.println(contextPane.visibleRows.get());
801        final int h = colHeaderPane.rowHeight.get() * colHeaderPane.visibleRows.get()
802            + contextPane.rowHeight.get() * contextPane.visibleRows.get();
803//        System.out.println(h);
804        return h;
805      }
806    };
807    rowHeaderPane.toFront();
808    colHeaderPane.toFront();
809    final Timeline t = new Timeline();
810    t.getKeyFrames().add(new KeyFrame(
811        Duration.millis(1000),
812        // new EventHandler<ActionEvent>() {
813        //
814        // public final void handle(final ActionEvent event) {
815        // final Timeline s = new Timeline();
816        // s.getKeyFrames().add(
817        // new KeyFrame(Duration.millis(1000), new KeyValue(zoomSlider.valueProperty(), 0.9d, Interpolator.EASE_OUT)));
818        // Platform.runLater(new Runnable() {
819        //
820        //
821        // public final void run() {
822        // s.play();
823        // }
824        // });
825        // }
826        // },
827        new KeyValue(zoomSlider.valueProperty(), 0d, Interpolator.EASE_IN)));
828    Platform.runLater(new Runnable() {
829
830      public void run() {
831        t.play();
832      }
833    });
834    if (dataset != null && dataset.editable) {
835
836      final Button domainButton = ButtonBuilder
837          .create()
838          // .text(conExpTab.fca.context.isHomogen() ? "New Element" : "New Object")
839          .onAction(new EventHandler<ActionEvent>() {
840
841            public void handle(ActionEvent event) {
842              ((FCADataset<String, String>) dataset).addObject(
843                  (dataset.context.isHomogen() ? "Element " : "Object ") + IdGenerator.getNextId(dataset),
844                  -1);
845            }
846          })
847          .build();
848      final ImageView view =
849          ImageViewBuilder.create().image(new Image(ConExpFX.class.getResourceAsStream("image/16x16/add.png"))).build();
850      view.scaleXProperty().bind(zoomFactor);
851      view.scaleYProperty().bind(zoomFactor);
852      domainButton.setGraphic(view);
853      domainButton.minWidthProperty().bind(rowHeaderPane.columnWidth);
854      domainButton.maxWidthProperty().bind(rowHeaderPane.columnWidth);
855      domainButton.minHeightProperty().bind(cellSize);
856      domainButton.maxHeightProperty().bind(cellSize);
857      domainButton.styleProperty().bind(new StringBinding() {
858
859        {
860          super.bind(textSize);
861        }
862
863        @Override
864        protected String computeValue() {
865          return "-fx-padding: 0; -fx-font-size: " + textSize.get() + ";";
866        }
867      });
868      centerPane.add(domainButton, 0, 2);
869      final Button codomainButton = ButtonBuilder
870          .create()
871          // .text(conExpTab.fca.context.isHomogen() ? "New Element" : "New Attribute")
872          .onAction(new EventHandler<ActionEvent>() {
873
874            public void handle(ActionEvent event) {
875              ((FCADataset<String, String>) dataset).addAttribute(
876                  (dataset.context.isHomogen() ? "Element " : "Attribute ") + IdGenerator.getNextId(dataset),
877                  -1);
878            }
879          })
880          .build();
881      final ImageView view2 =
882          ImageViewBuilder.create().image(new Image(ConExpFX.class.getResourceAsStream("image/16x16/add.png"))).build();
883      view2.scaleXProperty().bind(zoomFactor);
884      view2.scaleYProperty().bind(zoomFactor);
885      codomainButton.setGraphic(view2);
886      codomainButton.rotateProperty().set(-90);
887      codomainButton.minWidthProperty().bind(colHeaderPane.rowHeight);
888      codomainButton.maxWidthProperty().bind(colHeaderPane.rowHeight);
889      codomainButton.minHeightProperty().bind(cellSize);
890      codomainButton.maxHeightProperty().bind(cellSize);
891      codomainButton.translateXProperty().bind(new DoubleBinding() {
892
893        {
894          super.bind(colHeaderPane.rowHeight, cellSize);
895        }
896
897        @Override
898        protected double computeValue() {
899          return -(colHeaderPane.rowHeight.get() - cellSize.get()) / 2d;
900        }
901      });
902      codomainButton.styleProperty().bind(new StringBinding() {
903
904        {
905          super.bind(textSize);
906        }
907
908        @Override
909        protected String computeValue() {
910          return "-fx-padding: 0; -fx-font-size: " + textSize.get() + ";";
911        }
912      });
913      centerPane.add(codomainButton, 2, 0);
914    }
915  }
916
917  private final void createToolBar() {
918    zoomFactor.bind(new DoubleBinding() {
919
920      {
921        bind(zoomSlider.valueProperty());
922      }
923
924      protected double computeValue() {
925        return Math.pow(2d, zoomSlider.valueProperty().get());
926      }
927    });
928    final ToggleButton arrowsToggleButton =
929        new ToggleButton(Constants.DOWN_ARROW_CHARACTER + Constants.UP_ARROW_CHARACTER);
930    arrowsToggleButton.setSelected(false);
931    arrowsToggleButton.setMinHeight(24);
932    showArrows.bind(arrowsToggleButton.selectedProperty());
933    final ToggleButton pathsToggleButton =
934        new ToggleButton(Constants.DOWN_ARROW_CHARACTER + Constants.DOWN_ARROW_CHARACTER);
935    pathsToggleButton.setDisable(true);
936    pathsToggleButton.setSelected(false);
937    pathsToggleButton.setMinHeight(24);
938    showPaths.bind(pathsToggleButton.selectedProperty());
939    arrowsToggleButton.addEventHandler(ActionEvent.ACTION, new EventHandler<ActionEvent>() {
940
941      @Override
942      public final void handle(final ActionEvent event) {
943        if (showArrows.get()) {
944          pathsToggleButton.setDisable(false);
945        } else {
946          pathsToggleButton.setSelected(false);
947          pathsToggleButton.setDisable(true);
948        }
949      }
950    });
951    highlightToggleButton.setSelected(false);
952    highlightToggleButton.setMinHeight(24);
953    highlightToggleButton.setGraphic(
954        ImageViewBuilder.create().image(new Image(ConExpFX.class.getResourceAsStream("image/16x16/flag.png"))).build());
955    rowHeaderPane.highlight.bind(highlightToggleButton.selectedProperty());
956    colHeaderPane.highlight.bind(highlightToggleButton.selectedProperty());
957    contextPane.highlight.bind(highlightToggleButton.selectedProperty());
958    arrowsToggleButton.setStyle("-fx-background-radius: 5 0 0 5, 5 0 0 5, 4 0 0 4, 3 0 0 3;");
959    pathsToggleButton.setStyle("-fx-background-radius: 0, 0, 0, 0");
960    highlightToggleButton.setStyle("-fx-background-radius: 0 5 5 0, 0 5 5 0, 0 4 4 0, 0 3 3 0;");
961    HBox showBox = new HBox();
962    showBox.setPadding(new Insets(0d));
963    showBox.getChildren().addAll(arrowsToggleButton, pathsToggleButton, highlightToggleButton);
964    final ToolBar toolBar = new ToolBar();
965    toolBar.getItems().addAll(zoomSlider, showBox);
966    this.setTop(toolBar);
967    toolBar.toFront();
968  }
969
970  protected final void update() {
971    rowHeaderPane.updateContent();
972    colHeaderPane.updateContent();
973    contextPane.updateContent();
974  }
975
976  public final void dehighlight() {
977    rowHeaderPane.dehighlight();
978    colHeaderPane.dehighlight();
979    contextPane.dehighlight();
980    contextPane.highlightConcept.set(false);
981  }
982
983  public final void highlight(final Concept<G, M> concept) {
984    final Collection<Integer> domainIndices = context.rowHeads().indicesOf(concept.extent(), true);
985    final Collection<Integer> codomainIndices = context.colHeads().indicesOf(concept.intent(), true);
986    contextPane.highlightConcept.set(true);
987    rowHeaderPane.highlightConcept(Collections2.transform(domainIndices, new Function<Integer, Integer>() {
988
989      public final Integer apply(final Integer index) {
990        for (Entry<Integer, Integer> entry : contextPane.rowMap.get().entrySet())
991          if (entry.getValue().equals(index))
992            return entry.getKey();
993        return index;
994      }
995    }));
996    colHeaderPane.highlightConcept(Collections2.transform(codomainIndices, new Function<Integer, Integer>() {
997
998      public final Integer apply(final Integer index) {
999        for (Entry<Integer, Integer> entry : contextPane.columnMap.get().entrySet())
1000          if (entry.getValue().equals(index))
1001            return entry.getKey();
1002        return index;
1003      }
1004    }));
1005  }
1006
1007  public final void highlightImplication(final Implication<M> implication) {
1008
1009  }
1010
1011  public final void addRowDecoration(final G row, final Node decoration) {
1012    rowHeaderPane.addDecoration(row, decoration);
1013  }
1014}