All Downloads are FREE. Search and download functionalities are using the official Maven repository.

us.ihmc.scs2.sessionVisualizer.jfx.controllers.chart.ChartTable2D Maven / Gradle / Ivy

package us.ihmc.scs2.sessionVisualizer.jfx.controllers.chart;

import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import us.ihmc.log.LogTools;
import us.ihmc.scs2.definition.yoChart.YoChartConfigurationDefinition;
import us.ihmc.scs2.definition.yoChart.YoChartGroupConfigurationDefinition;
import us.ihmc.scs2.sessionVisualizer.jfx.charts.ChartIdentifier;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Supplier;

public class ChartTable2D
{
   private boolean ignoreSizePropertyListener = false;
   private final Property size = new SimpleObjectProperty<>(new ChartTable2DSize(0, 0));
   private final Property maxSize = new SimpleObjectProperty<>(new ChartTable2DSize(6, 6));
   private final Supplier chartBuilder;

   private final List listeners = new ArrayList<>();

   private YoChartPanelController[][] chartTable = new YoChartPanelController[0][0];

   public ChartTable2D(Supplier chartBuilder)
   {
      this.chartBuilder = chartBuilder;

      size.addListener((o, oldValue, newValue) ->
                       {
                          if (ignoreSizePropertyListener)
                             return;
                          resize(oldValue, newValue, false);
                       });
   }

   public boolean isEmpty()
   {
      if (getSize().isEmpty())
         return true;

      for (int row = 0; row < getSize().getNumberOfRows(); row++)
      {
         for (int col = 0; col < getSize().getNumberOfCols(); col++)
         {
            if (!isChartEmpty(row, col))
               return false;
         }
      }
      return true;
   }

   public ChartTable2DSize getSize()
   {
      return sizeProperty().getValue();
   }

   public Property sizeProperty()
   {
      return size;
   }

   public ChartTable2DSize getMaxSize()
   {
      return maxSizeProperty().getValue();
   }

   public Property maxSizeProperty()
   {
      return maxSize;
   }

   public boolean set(YoChartGroupConfigurationDefinition definition)
   {
      if (!maxSize.getValue().contains(definition.getNumberOfRows() - 1, definition.getNumberOfColumns() - 1))
      {
         LogTools.warn("Cannot set from configuration, required number of rows/columns is too large.");
         return false;
      }

      clear(); // TODO we can do without clearing and instead recycle charts if any.

      ChartTable2DSize newSize = new ChartTable2DSize(definition.getNumberOfRows(), definition.getNumberOfColumns());
      YoChartPanelController[][] newTable = new YoChartPanelController[newSize.getNumberOfRows()][newSize.getNumberOfCols()];
      YoChartConfigurationDefinition[][] newTableDefinition = extractChartDefinitionTable2D(definition);

      for (int row = 0; row < newSize.getNumberOfRows(); row++)
      {
         for (int col = 0; col < newSize.getNumberOfCols(); col++)
         {
            YoChartPanelController newChart = chartBuilder.get();
            newTable[row][col] = newChart;
            notifyChartAdded(newChart, row, col); // Listener first, so listener on chart plotted variables can be added.
            newChart.setChartConfiguration(newTableDefinition[row][col]);
         }
      }
      chartTable = newTable;
      ignoreSizePropertyListener = true;
      size.setValue(newSize);
      ignoreSizePropertyListener = false;
      return true;
   }

   private static YoChartConfigurationDefinition[][] extractChartDefinitionTable2D(YoChartGroupConfigurationDefinition definition)
   {
      YoChartConfigurationDefinition[][] tableDefinition = new YoChartConfigurationDefinition[definition.getNumberOfRows()][definition.getNumberOfColumns()];

      if (definition.getChartConfigurations() != null)
      {
         for (YoChartConfigurationDefinition chartDefinition : definition.getChartConfigurations())
         {
            tableDefinition[chartDefinition.getIdentifier().getRow()][chartDefinition.getIdentifier().getColumn()] = chartDefinition;
         }
      }

      return tableDefinition;
   }

   public void addListener(ChartChangeListener listener)
   {
      listeners.add(listener);
   }

   private void notifyChartAdded(YoChartPanelController chart, int row, int col)
   {
      ChartChange change = new ChartChange(ChangeType.ADD, chart, null, new ChartIdentifier(row, col));

      for (ChartChangeListener listener : listeners)
         listener.onChange(change);
   }

   private void notifyChartRemoved(YoChartPanelController chart, int row, int col)
   {
      if (chart == null)
         return;

      ChartChange change = new ChartChange(ChangeType.REMOVE, chart, new ChartIdentifier(row, col), null);

      for (ChartChangeListener listener : listeners)
         listener.onChange(change);
   }

   private void notifyChartMoved(YoChartPanelController chart, int fromRow, int fromCol, int toRow, int toCol)
   {
      if (chart == null)
         return;
      if (fromRow == toRow && fromCol == toCol)
         return; // Nothing moved really.

      ChartChange change = new ChartChange(ChangeType.MOVE, chart, new ChartIdentifier(fromRow, fromCol), new ChartIdentifier(toRow, toCol));

      for (ChartChangeListener listener : listeners)
         listener.onChange(change);
   }

   public void clear()
   {
      for (int row = 0; row < size.getValue().getNumberOfRows(); row++)
      {
         for (int col = 0; col < size.getValue().getNumberOfCols(); col++)
         {
            removeChart(row, col);
         }
      }
      chartTable = new YoChartPanelController[0][0];
      ignoreSizePropertyListener = true;
      size.setValue(new ChartTable2DSize(0, 0));
      ignoreSizePropertyListener = false;
   }

   public ChartTable2DSize resize(ChartTable2DSize desiredSize)
   {
      return resize(desiredSize, false);
   }

   private ChartTable2DSize resize(ChartTable2DSize desiredSize, boolean onlyConsiderNullForDownsize)
   {
      ChartTable2DSize newSize = resize(size.getValue(), desiredSize, onlyConsiderNullForDownsize);
      ignoreSizePropertyListener = true;
      size.setValue(newSize);
      ignoreSizePropertyListener = false;
      return newSize;
   }

   private ChartTable2DSize resize(ChartTable2DSize oldSize, ChartTable2DSize desiredSize, boolean onlyConsiderNullForDownsize)
   {
      if (chartTable.length != oldSize.getNumberOfRows() || (chartTable.length == 0 ? 0 : chartTable[0].length) != oldSize.getNumberOfCols())
         throw new IllegalStateException(String.format("Unexpected chartTable size: was [row=%d, col=%d], expected [row=%d, col=%d]",
                                                       chartTable.length,
                                                       chartTable.length == 0 ? 0 : chartTable[0].length,
                                                       oldSize.getNumberOfRows(),
                                                       oldSize.getNumberOfCols()));

      if (Objects.equals(oldSize, desiredSize))
         return desiredSize;

      if (!maxSize.getValue().contains(desiredSize))
      {
         LogTools.warn("Trying to set the size ({}) to be bigger than the max size ({}), reverting resize.", desiredSize, maxSize);
         return oldSize;
      }

      if (oldSize.isEmpty())
      { // Starting from a blank slate. Just pad with empty charts
         chartTable = new YoChartPanelController[desiredSize.getNumberOfRows()][desiredSize.getNumberOfCols()];

         for (int row = 0; row < desiredSize.getNumberOfRows(); row++)
         {
            for (int col = 0; col < desiredSize.getNumberOfCols(); col++)
            {
               notifyChartAdded(chartTable[row][col] = chartBuilder.get(), row, col);
            }
         }
      }
      else if (desiredSize.getNumberOfRows() >= oldSize.getNumberOfRows())
      {
         if (desiredSize.getNumberOfCols() > oldSize.getNumberOfCols())
         { // Only increasing, just pad new rows and columns with new charts.
            YoChartPanelController[][] newTable = new YoChartPanelController[desiredSize.getNumberOfRows()][desiredSize.getNumberOfCols()];

            for (int row = 0; row < desiredSize.getNumberOfRows(); row++)
            {
               for (int col = 0; col < desiredSize.getNumberOfCols(); col++)
               {
                  if (oldSize.contains(row, col)) // Copy the current table to the new one
                     newTable[row][col] = chartTable[row][col];
                  else // Pad the new rows
                     notifyChartAdded(newTable[row][col] = chartBuilder.get(), row, col);
               }
            }
            chartTable = newTable;
         }
         else
         { // Increasing rows and decreasing columns.
            // Figure out if it is possible to decrease the number of columns as desired.
            int minCols = computeMinColumns(oldSize, onlyConsiderNullForDownsize);
            if (minCols > desiredSize.getNumberOfCols())
            { // Can't reduce as much, doing best effort.
               desiredSize = new ChartTable2DSize(desiredSize.getNumberOfRows(), minCols);
               if (oldSize.equals(desiredSize))
                  return oldSize; // Nothing to do.
            }

            YoChartPanelController[][] newTable = new YoChartPanelController[desiredSize.getNumberOfRows()][desiredSize.getNumberOfCols()];

            // To keep track of the number of columns to be removed, so we can do a lazy removal.
            int colsToRemove = oldSize.getNumberOfCols() - desiredSize.getNumberOfCols();

            int newCol = desiredSize.getNumberOfCols() - 1;

            for (int oldCol = oldSize.getNumberOfCols() - 1; oldCol >= 0; oldCol--)
            {
               // Only remove the desired number of columns, not necessarily trying to reach min size.
               if (colsToRemove > 0 && isColumnEmpty(oldCol, oldSize, onlyConsiderNullForDownsize))
               { // We won't use these charts, notify listeners that they are removed.
                  colsToRemove--;
                  for (int row = 0; row < oldSize.getNumberOfRows(); row++)
                     notifyChartRemoved(chartTable[row][oldCol], row, oldCol);
                  continue;
               }

               for (int row = 0; row < desiredSize.getNumberOfRows(); row++)
               {
                  if (row < oldSize.getNumberOfRows()) // Shift the column
                     notifyChartMoved(newTable[row][newCol] = chartTable[row][oldCol], row, oldCol, row, newCol);
                  else // Pad the new rows with new empty charts
                     notifyChartAdded(newTable[row][newCol] = chartBuilder.get(), row, newCol);
               }
               newCol--;
            }
            chartTable = newTable;
         }
      }
      else if (desiredSize.getNumberOfCols() >= oldSize.getNumberOfCols())
      { // Increasing number of columns and decreasing number of rows.
         int minRows = computeMinRows(oldSize, onlyConsiderNullForDownsize);
         if (minRows > desiredSize.getNumberOfRows())
         { // Can't reduce as much, doing best effort.
            desiredSize = new ChartTable2DSize(minRows, desiredSize.getNumberOfCols());
            if (oldSize.equals(desiredSize))
               return oldSize; // Nothing to do.
         }

         YoChartPanelController[][] newTable = new YoChartPanelController[desiredSize.getNumberOfRows()][desiredSize.getNumberOfCols()];

         // To keep track of the number of rows to be removed, so we can do a lazy removal.
         int rowsToRemove = oldSize.getNumberOfRows() - desiredSize.getNumberOfRows();

         int newRow = desiredSize.getNumberOfRows() - 1;

         for (int oldRow = oldSize.getNumberOfRows() - 1; oldRow >= 0; oldRow--)
         {
            // Only remove the desired number of rows, not trying to reach min size.
            if (rowsToRemove > 0 && isRowEmpty(oldRow, oldSize, onlyConsiderNullForDownsize))
            { // We won't use these charts, notify listeners that they are removed.
               rowsToRemove--;
               for (int col = 0; col < oldSize.getNumberOfCols(); col++)
                  notifyChartRemoved(chartTable[oldRow][col], oldRow, col);
               continue;
            }

            for (int col = 0; col < desiredSize.getNumberOfCols(); col++)
            {
               if (col < oldSize.getNumberOfCols()) // Shift the row
                  notifyChartMoved(newTable[newRow][col] = chartTable[oldRow][col], oldRow, col, newRow, col);
               else // Pad with new charts
                  notifyChartAdded(newTable[newRow][col] = chartBuilder.get(), newRow, col);
            }
            newRow--;
         }
         chartTable = newTable;
      }
      else
      { // Decreasing both rows and columns
         int minRows = computeMinRows(oldSize, onlyConsiderNullForDownsize);
         int minCols = computeMinColumns(oldSize, onlyConsiderNullForDownsize);

         if (minRows > desiredSize.getNumberOfRows() || minCols > desiredSize.getNumberOfCols())
         { // Can't reduce as much, doing best effort.
            desiredSize = new ChartTable2DSize(minRows, minCols);
            if (oldSize.equals(desiredSize))
               return oldSize; // Nothing to do.
         }

         YoChartPanelController[][] newTable = new YoChartPanelController[desiredSize.getNumberOfRows()][desiredSize.getNumberOfCols()];

         // To keep track of the number of rows to be removed, so we can do a lazy removal.
         int rowsToRemove = oldSize.getNumberOfRows() - desiredSize.getNumberOfRows();

         int newRow = desiredSize.getNumberOfRows() - 1;

         for (int oldRow = oldSize.getNumberOfRows() - 1; oldRow >= 0; oldRow--)
         {
            if (rowsToRemove > 0 && isRowEmpty(oldRow, oldSize, onlyConsiderNullForDownsize))
            { // We won't use these charts, notify listeners that they are removed.
               rowsToRemove--;
               for (int oldCol = 0; oldCol < oldSize.getNumberOfCols(); oldCol++)
                  notifyChartRemoved(chartTable[oldRow][oldCol], oldRow, oldCol);
               continue;
            }

            // To keep track of the number of cols to be removed, so we can do a lazy removal.
            int colsToRemove = oldSize.getNumberOfCols() - desiredSize.getNumberOfCols();
            int newCol = desiredSize.getNumberOfCols() - 1;

            for (int oldCol = oldSize.getNumberOfCols() - 1; oldCol >= 0; oldCol--)
            {
               if (colsToRemove > 0 && isColumnEmpty(oldCol, oldSize, onlyConsiderNullForDownsize)) // TODO This can be optimized.
               { // We won't use this chart, notify listeners that it is removed.
                  colsToRemove--;
                  notifyChartRemoved(chartTable[oldRow][oldCol], oldRow, oldCol);
                  continue;
               }

               // Shift this chart in the grid
               notifyChartMoved(newTable[newRow][newCol] = chartTable[oldRow][oldCol], oldRow, oldCol, newRow, newCol);
               newCol--;
            }
            newRow--;
         }

         chartTable = newTable;
      }
      return desiredSize;
   }

   public void removeNullRowsAndColumns()
   {
      resize(new ChartTable2DSize(0, 0), true);
   }

   public YoChartPanelController get(int row, int col)
   {
      return chartTable[row][col];
   }

   public boolean isChartEmpty(int row, int col)
   {
      YoChartPanelController chart = chartTable[row][col];
      return chart == null || chart.isEmpty();
   }

   public boolean removeChart(YoChartPanelController chart)
   {
      for (int row = 0; row < size.getValue().getNumberOfRows(); row++)
      {
         for (int col = 0; col < size.getValue().getNumberOfCols(); col++)
         {
            if (chart == chartTable[row][col])
            {
               chartTable[row][col] = null;
               notifyChartRemoved(chart, row, col);
               return true;
            }
         }
      }
      return false;
   }

   public void removeChart(ChartIdentifier id)
   {
      removeChart(id.getRow(), id.getColumn());
   }

   public void removeChart(int row, int col)
   {
      YoChartPanelController chartToRemove = chartTable[row][col];
      chartTable[row][col] = null;
      notifyChartRemoved(chartToRemove, row, col);
   }

   public void removeEmptyCharts()
   {
      for (int row = 0; row < getSize().getNumberOfRows(); row++)
      {
         for (int col = 0; col < getSize().getNumberOfCols(); col++)
         {
            if (isChartEmpty(row, col))
               removeChart(row, col);
         }
      }
   }

   public ChartTable2DSize computeMinSize(ChartTable2DSize currentSize, boolean onlyNull)
   {
      return new ChartTable2DSize(computeMinRows(currentSize, onlyNull), computeMinColumns(currentSize, onlyNull));
   }

   private int computeMinRows(ChartTable2DSize currentSize, boolean onlyNull)
   {
      return currentSize.getNumberOfRows() - numberOfEmptyRows(currentSize, onlyNull);
   }

   private int computeMinColumns(ChartTable2DSize currentSize, boolean onlyNull)
   {
      return currentSize.getNumberOfCols() - numberOfEmptyColumns(currentSize, onlyNull);
   }

   private int numberOfEmptyRows(ChartTable2DSize size, boolean onlyNull)
   {
      int count = 0;
      for (int row = 0; row < size.getNumberOfRows(); row++)
      {
         if (isRowEmpty(row, size, onlyNull))
            count++;
      }
      return count;
   }

   private boolean isRowEmpty(int row, ChartTable2DSize size, boolean onlyNull)
   {
      if (row < 0 || row > size.getNumberOfRows())
         throw new IndexOutOfBoundsException(String.format("Row (%d) is out of bound, expected range [0,%d[", row, size.getNumberOfRows()));

      YoChartPanelController[] chartRow = chartTable[row];

      for (int col = 0; col < chartRow.length; col++)
      {
         YoChartPanelController chart = chartRow[col];
         if (onlyNull)
         {
            if (chart != null)
               return false;
         }
         else
         {
            if (chart != null && !chart.isEmpty())
               return false;
         }
      }

      return true;
   }

   private int numberOfEmptyColumns(ChartTable2DSize size, boolean onlyNull)
   {
      int count = 0;
      for (int col = 0; col < chartTable[0].length; col++)
      {
         if (isColumnEmpty(col, size, onlyNull))
            count++;
      }
      return count;
   }

   private boolean isColumnEmpty(int col, ChartTable2DSize size, boolean onlyNull)
   {
      if (col < 0 || col > size.getNumberOfCols())
         throw new IndexOutOfBoundsException(String.format("Col (%d) is out of bound, expected range [0,%d[", col, size.getNumberOfCols()));

      for (int row = 0; row < chartTable.length; row++)
      {
         YoChartPanelController chart = chartTable[row][col];
         if (onlyNull)
         {
            if (chart != null)
               return false;
         }
         else
         {
            if (chart != null && !chart.isEmpty())
               return false;
         }
      }
      return true;
   }

   public void forEachChart(Consumer action)
   {
      for (int row = 0; row < getSize().getNumberOfRows(); row++)
      {
         for (int col = 0; col < getSize().getNumberOfCols(); col++)
         {
            YoChartPanelController chart = get(row, col);
            if (chart != null)
               action.accept(chart);
         }
      }
   }

   public List toChartDefinitions()
   {
      List chartDefinitions = new ArrayList<>();
      for (int row = 0; row < getSize().getNumberOfRows(); row++)
      {
         for (int col = 0; col < getSize().getNumberOfCols(); col++)
         {
            if (!isChartEmpty(row, col))
               chartDefinitions.add(get(row, col).toYoChartConfigurationDefinition(new ChartIdentifier(row, col)));
         }
      }
      return chartDefinitions;
   }

   public static interface ChartChangeListener
   {
      void onChange(ChartChange c);
   }

   public enum ChangeType
   {
      ADD, REMOVE, MOVE
   }

   public static class ChartChange
   {
      private final ChangeType type;
      private final YoChartPanelController chart;
      private final ChartIdentifier from, to;

      private ChartChange(ChangeType type, YoChartPanelController chart, ChartIdentifier from, ChartIdentifier to)
      {
         this.type = type;
         this.chart = chart;
         this.from = from;
         this.to = to;
      }

      public ChangeType type()
      {
         return type;
      }

      public YoChartPanelController getChart()
      {
         return chart;
      }

      public ChartIdentifier from()
      {
         return from;
      }

      public int fromRow()
      {
         return from == null ? -1 : from.getRow();
      }

      public int fromCol()
      {
         return from == null ? -1 : from.getColumn();
      }

      public ChartIdentifier to()
      {
         return to;
      }

      public int toRow()
      {
         return to == null ? -1 : to.getRow();
      }

      public int toCol()
      {
         return to == null ? -1 : to.getColumn();
      }

      @Override
      public String toString()
      {
         switch (type)
         {
            case ADD:
               return String.format("Add    - [row=%d, col=%d]", toRow(), toCol());
            case REMOVE:
               return String.format("Remove - [row=%d, col=%d]", fromRow(), fromCol());
            case MOVE:
               return String.format("Move   - [row=%d, col=%d] => [row=%d, col=%d]", fromRow(), fromCol(), toRow(), toCol());
            default:
               throw new IllegalArgumentException("Unexpected value: " + type);
         }
      }
   }

   public static class ChartTable2DSize
   {
      private final int numberOfRows;
      private final int numberOfCols;

      public ChartTable2DSize(int numberOfRows, int numberOfCols)
      {
         this.numberOfRows = numberOfRows;
         this.numberOfCols = numberOfCols;
      }

      public int getNumberOfRows()
      {
         return numberOfRows;
      }

      public int getNumberOfCols()
      {
         return numberOfCols;
      }

      public boolean isEmpty()
      {
         return numberOfRows == 0 && numberOfCols == 0;
      }

      public boolean contains(int row, int col)
      {
         return row < numberOfRows && col < numberOfCols;
      }

      public boolean contains(ChartTable2DSize other)
      {
         return contains(other.numberOfRows - 1, other.numberOfCols - 1);
      }

      @Override
      public boolean equals(Object object)
      {
         if (object == this)
            return true;
         else if (object instanceof ChartTable2DSize other)
            return numberOfRows == other.numberOfRows && numberOfCols == other.numberOfCols;
         else
            return false;
      }

      @Override
      public String toString()
      {
         return String.format("Size: [nRows=%d, nCols=%d]", numberOfRows, numberOfCols);
      }
   }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy