/**
 * {@link ChartType#PLAYER_STATS} chart factory.
 *
 * @author Andras Belicza
 */
public class PlayerStatsChartFactory extends BaseChartFactory<LineChartDataSet> {

  /**
   * Player stat.
   *
   * @author Andras Belicza
   */
  public enum Stat {

    /** Resources Current (min+gas). */
    RES_CURRENT(
        "Resources Current", true, PlayerStatsEvent.F_MINS_CURRENT, PlayerStatsEvent.F_GAS_CURRENT),

    /** Resource Collection Rate (min+gas). */
    RES_COLL_RATE(
        "Resource Collection Rate",
        true,
        PlayerStatsEvent.F_MINS_COLL_RATE,
        PlayerStatsEvent.F_GAS_COLL_RATE),

    /** Spending Quotient (SQ) (min+gas). */
    SPENDING_QUOTIENT("Spending Quotient (SQ)", true, "minsDummy", "gasDummy"),

    /** Workers Active. */
    WORKERS_ACTIVE("Workers Active", false, PlayerStatsEvent.F_WORKERS_ACTIVE_COUNT),

    /** Food Used. */
    FOOD_USED("Food Used", false, PlayerStatsEvent.F_FOOD_USED),

    /** Food Used. */
    FOOD_MADE("Food Made", false, PlayerStatsEvent.F_FOOD_MADE),

    /** Food Made+Used. */
    FOOD_MADE_USED("Food Made+Used", false, "scoreValueFoodDummy"),

    /** Army Resources Current. */
    ARMY_RES_CURRENT(
        "Army Resources Current",
        true,
        PlayerStatsEvent.F_MINS_USED_IN_CURRENT_ARMY,
        PlayerStatsEvent.F_GAS_USED_IN_CURRENT_ARMY),

    /** Army Resources Current. */
    ARMY_RES_LOST(
        "Army Resources Lost",
        true,
        PlayerStatsEvent.F_MINS_LOST_ARMY,
        PlayerStatsEvent.F_GAS_LOST_ARMY),

    /** Army Resources Killed. */
    ARMY_RES_KILLED(
        "Army Resources Killed",
        true,
        PlayerStatsEvent.F_MINS_KILLED_ARMY,
        PlayerStatsEvent.F_GAS_KILLED_ARMY),

    /** Army Resources In Progress. */
    ARMY_RES_IN_PROGRESS(
        "Army Resources In Progress",
        true,
        PlayerStatsEvent.F_MINS_USED_IN_PROGRESS_ARMY,
        PlayerStatsEvent.F_GAS_USED_IN_PROGRESS_ARMY),

    /** Economy Resources Current. */
    ECON_RES_CURRENT(
        "Economy Resources Current",
        true,
        PlayerStatsEvent.F_MINS_USED_IN_CURRENT_ECON,
        PlayerStatsEvent.F_GAS_USED_IN_CURRENT_ECON),

    /** Economy Resources Current. */
    ECON_RES_LOST(
        "Economy Resources Lost",
        true,
        PlayerStatsEvent.F_MINS_LOST_ECON,
        PlayerStatsEvent.F_GAS_LOST_ECON),

    /** Economy Resources Killed. */
    ECON_RES_KILLED(
        "Economy Resources Killed",
        true,
        PlayerStatsEvent.F_MINS_KILLED_ECON,
        PlayerStatsEvent.F_GAS_KILLED_ECON),

    /** Economy Resources In Progress. */
    ECON_RES_IN_PROGRESS(
        "Economy Resources In Progress",
        true,
        PlayerStatsEvent.F_MINS_USED_IN_PROGRESS_ECON,
        PlayerStatsEvent.F_GAS_USED_IN_PROGRESS_ECON),

    /** Technology Resources Current. */
    TECH_RES_CURRENT(
        "Technology Resources Current",
        true,
        PlayerStatsEvent.F_MINS_USED_IN_CURRENT_TECH,
        PlayerStatsEvent.F_GAS_USED_IN_CURRENT_TECH),

    /** Technology Resources Current. */
    TECH_RES_LOST(
        "Technology Resources Lost",
        true,
        PlayerStatsEvent.F_MINS_LOST_TECH,
        PlayerStatsEvent.F_GAS_LOST_TECH),

    /** Technology Resources Killed. */
    TECH_RES_KILLED(
        "Technology Resources Killed",
        true,
        PlayerStatsEvent.F_MINS_KILLED_TECH,
        PlayerStatsEvent.F_GAS_KILLED_TECH),

    /** Technology Resources In Progress. */
    TECH_RES_IN_PROGRESS(
        "Technology Resources In Progress",
        true,
        PlayerStatsEvent.F_MINS_USED_IN_PROGRESS_TECH,
        PlayerStatsEvent.F_GAS_USED_IN_PROGRESS_TECH);

    /** Text value of the player controller. */
    public final String text;

    /** Tells if the stat includes minerals and vespene. */
    public final boolean hasMinGas;

    /**
     * Event fields associated with this stat.<br>
     * If stat is minerals and vespene, it contains 2 values for minerals and vespene.
     */
    public final String[] eventFields;

    /**
     * Creates a new {@link Stat}.
     *
     * @param text text of the presentation
     * @param hasMinGas tells if the stat includes minerals and vespene
     * @param eventFields event fields associated with this stat
     */
    private Stat(final String text, final boolean hasMinGas, final String... eventFields) {
      this.text = text;
      this.hasMinGas = hasMinGas;
      this.eventFields = eventFields;

      if (Env.DEV_MODE)
        if (hasMinGas && eventFields.length != 2)
          throw new RuntimeException(
              "Stats that has minerals and vespene must have exactly 2 event fields!");
    }

    @Override
    public String toString() {
      return text;
    }

    /** Cache of the values array. */
    public static final Stat[] VALUES = values();
  }

  /** Combo box to select the presentation. */
  private final XComboBox<Stat> statComboBox =
      SettingsGui.createSettingComboBox(Settings.PLAYER_STATS_STAT, Env.APP_SETTINGS, null);

  /** Combo box to select the presentation. */
  private final XComboBox<Presentation> presentationComboBox =
      SettingsGui.createSettingComboBox(Settings.PLAYER_STATS_PRESENTATION, Env.APP_SETTINGS, null);

  /** Check box to display / include minerals. */
  private final XCheckBox minsCheckBox =
      SettingsGui.createSettingCheckBox(Settings.PLAYER_STATS_INCLUDE_MINS, Env.APP_SETTINGS, null);

  /** Check box to display / include vespene. */
  private final XCheckBox gasCheckBox =
      SettingsGui.createSettingCheckBox(Settings.PLAYER_STATS_INCLUDE_GAS, Env.APP_SETTINGS, null);

  /** Check box to separate minerals and vespene. */
  private final XCheckBox separateMinsGasCheckBox =
      SettingsGui.createSettingCheckBox(
          Settings.PLAYER_STATS_SEPARATE_MINS_GAS, Env.APP_SETTINGS, null);

  /**
   * Creates a new {@link PlayerStatsChartFactory}.
   *
   * @param chartsComp reference to the charts component that created us
   */
  public PlayerStatsChartFactory(final ChartsComp chartsComp) {
    super(chartsComp);

    statComboBox.addActionListener(chartsComp.chartsRebuilder);
    statComboBox.addActionListener(
        new ActionAdapter(true) {
          @Override
          public void actionPerformed(final ActionEvent event) {
            final boolean hasMinGas = statComboBox.getSelectedItem().hasMinGas;
            minsCheckBox.setVisible(hasMinGas);
            gasCheckBox.setVisible(hasMinGas);
            separateMinsGasCheckBox.setVisible(hasMinGas);
          }
        });
    statComboBox.markSeparatedItems(
        Stat.FOOD_MADE_USED, Stat.ARMY_RES_IN_PROGRESS, Stat.ECON_RES_IN_PROGRESS);
    statComboBox.setMaximumRowCount(statComboBox.getItemCount());

    presentationComboBox.addActionListener(chartsComp.chartsReconfigurer);

    minsCheckBox.setText("Minerals");
    minsCheckBox.setToolTipText(Settings.PLAYER_STATS_INCLUDE_MINS.name);
    minsCheckBox.addActionListener(chartsComp.chartsRebuilder);

    gasCheckBox.setText("Gas");
    gasCheckBox.setToolTipText(Settings.PLAYER_STATS_INCLUDE_GAS.name);
    gasCheckBox.addActionListener(chartsComp.chartsRebuilder);

    separateMinsGasCheckBox.setText("Separate Mins/Gas");
    separateMinsGasCheckBox.setToolTipText(Settings.PLAYER_STATS_SEPARATE_MINS_GAS.name);
    separateMinsGasCheckBox.addActionListener(chartsComp.chartsRebuilder);
  }

  @Override
  public void addChartOptions(final XToolBar toolBar) {
    toolBar.add(new HelpIcon(Helps.PLAYER_STATS_CHART, true).rightBorder(5));

    toolBar.add(new XLabel(Settings.PLAYER_STATS_STAT.name + ":"));
    toolBar.add(statComboBox);

    toolBar.add(new XLabel(Settings.PLAYER_STATS_PRESENTATION.name + ":"));
    toolBar.add(presentationComboBox);

    toolBar.add(minsCheckBox);

    toolBar.add(gasCheckBox);

    toolBar.add(separateMinsGasCheckBox);
  }

  @Override
  public List<Chart<LineChartDataSet>> createCharts() {
    chartList = new ArrayList<>();

    final TrackerEvents trackerEvents = repProc.replay.trackerEvents;
    if (trackerEvents == null) {
      chartsComp
          .getChartsCanvas()
          .setMessage(
              "This chart is available only from replay version 2.0.8. This replay has version "
                  + repProc.replay.header.versionString(false)
                  + ".");
      return chartList;
    }

    createModelByPlayerIds();

    switch (statComboBox.getSelectedItem()) {
      case SPENDING_QUOTIENT:
        // Spending Quotient is different than the others: it is not stored directly in the tracker
        // events stream
        calculateSQDataSets();
        break;
      case FOOD_MADE_USED:
        // Food Made+Used is showing 2 player stats at once: Food Made and Food Used
        calculateFoodMadeUsedDataSets();
        break;
      default:
        calculateDataSets();
        break;
    }

    return chartList;
  }

  @Override
  protected LineTimeChart createChart() {
    return new LineTimeChart(repProc);
  }

  @Override
  protected DataModel<LineChartDataSet> createChartModel(final User user, final String modelTitle) {
    final DataModel<LineChartDataSet> model =
        new DataModel<>(modelTitle, user.getPlayerColor().brighterColor, user);

    final Stat stat = statComboBox.getSelectedItem();

    int dsCount = 0;
    boolean gasOnly = false;
    if (stat.hasMinGas) {
      if (separateMinsGasCheckBox.isSelected()) {
        if (minsCheckBox.isSelected()) dsCount++;
        else gasOnly = true;
        if (gasCheckBox.isSelected()) dsCount++;
      } else dsCount = minsCheckBox.isSelected() || gasCheckBox.isSelected() ? 1 : 0;
    } else dsCount = stat.eventFields.length;

    for (int i = 0; i < dsCount; i++) {
      // Stroke will be set during reconfiguration
      final LineChartDataSet ds =
          new LineChartDataSet(
              i > 0 || gasOnly ? user.getPlayerColor().brighterColor : user.getPlayerColor().color,
              null,
              new int[0],
              new int[0]);
      model.addDataSet(ds);
    }

    return model;
  }

  /** Calculates the data sets. */
  private void calculateDataSets() {
    calculateDataSets(statComboBox.getSelectedItem());
  }

  /** Calculates the Spending Quotient data sets. */
  private void calculateSQDataSets() {
    // Spending Quotient is a function of 2 other Stats: SQ = f( RES_CURRENT, RES_COLL_RATE )

    // First calculate RES_CURRENT data set...
    calculateDataSets(Stat.RES_CURRENT);
    // ...and clone it because we'll need it in a moment
    final List<Chart<LineChartDataSet>> clonedChartList = new ArrayList<>(chartList.size());
    for (final Chart<LineChartDataSet> chart : chartList) {
      final Chart<LineChartDataSet> clonedChart = createChart();

      for (final DataModel<LineChartDataSet> model : chart.getDataModelList()) {
        final DataModel<LineChartDataSet> clonedModel = new DataModel<>(null, null);

        for (final LineChartDataSet dataSet : model.getDataSetList())
          clonedModel.addDataSet(
              new LineChartDataSet(null, null, dataSet.getLoops(), dataSet.getValues()));

        clonedChart.addModel(clonedModel);
      }

      clonedChartList.add(clonedChart);
    }

    // Cloning done, let's roll on to calculating the RES_COLL_RATE data set...
    calculateDataSets(Stat.RES_COLL_RATE);

    // We'll need the last command game event loops indexed by data model (last cmd loop is the same
    // for all data sets in a
    // model)
    final Map<DataModel<LineChartDataSet>, Int> modelLastCmdLoopsMap = new HashMap<>();
    for (final Event event : repProc.replay.gameEvents.events) {
      if (event.id != IGameEvents.ID_CMD) continue;

      // Player stats go by tracker events which go by player id. First check if there is player for
      // the user!
      final int playerId = repProc.usersByUserId[event.userId].playerId;
      if (playerId <= 0) continue;

      final DataModel<LineChartDataSet> model = modelByPlayerIds[playerId];
      if (model == null) continue;

      // Update last command loop per model
      Int lastCmdLoop = modelLastCmdLoopsMap.get(model);
      if (lastCmdLoop == null) modelLastCmdLoopsMap.put(model, lastCmdLoop = new Int());
      lastCmdLoop.value = event.loop;
    }

    // And the final step: "merge" the 2 data sets, the result being the SQ
    for (int chartIdx = 0; chartIdx < chartList.size(); chartIdx++) {
      final Chart<LineChartDataSet> chart = chartList.get(chartIdx);
      final Chart<LineChartDataSet> clonedChart = clonedChartList.get(chartIdx);

      for (int modelIdx = 0; modelIdx < chart.getDataModelList().size(); modelIdx++) {
        final DataModel<LineChartDataSet> model = chart.getDataModelList().get(modelIdx);
        final DataModel<LineChartDataSet> clonedModel =
            clonedChart.getDataModelList().get(modelIdx);

        final int lastCmdLoop =
            modelLastCmdLoopsMap.get(model) == null ? 0 : modelLastCmdLoopsMap.get(model).value;

        for (int dataSetIdx = 0; dataSetIdx < model.getDataSetList().size(); dataSetIdx++) {
          final LineChartDataSet dataSet = model.getDataSetList().get(dataSetIdx);
          final LineChartDataSet clonedDataSet = clonedModel.getDataSetList().get(dataSetIdx);

          // Only include values in total before the last command loop! And of course only count
          // samples up to that.

          final int[] loops = dataSet.getLoops();
          final int[] values = dataSet.getValues();
          final int[] clonedValues = clonedDataSet.getValues();

          long totalValues = 0, totalClonedValues = 0;
          int totalCount = 0;

          for (int valueIdx = 0; valueIdx < values.length; valueIdx++) {
            if (loops[valueIdx] <= lastCmdLoop) {
              totalClonedValues += clonedValues[valueIdx];
              totalValues += values[valueIdx];
              totalCount++;
            }
            values[valueIdx] =
                RepProcessor.calculateSQImpl(clonedValues[valueIdx], values[valueIdx]);
          }

          // Calculate average SQ for the data set, put in data set title!
          final int avgSQ =
              totalCount == 0
                  ? 0
                  : RepProcessor.calculateSQImpl(
                      (int) (totalClonedValues / totalCount), (int) (totalValues / totalCount));
          dataSet.setTitle("SQ: " + avgSQ);
          dataSet.calculateValueMax();
        }
      }
    }
  }

  /** Calculates the Food Made+Used data sets. */
  private void calculateFoodMadeUsedDataSets() {
    // Food Made+Used is showing 2 player stats at once: Food Made and Food Used

    // First calculate FOOD_USED data set...
    calculateDataSets(Stat.FOOD_USED);
    // ...and clone it because we'll need it in a moment
    final List<Chart<LineChartDataSet>> clonedChartList = new ArrayList<>(chartList.size());
    for (final Chart<LineChartDataSet> chart : chartList) {
      final Chart<LineChartDataSet> clonedChart = createChart();

      for (final DataModel<LineChartDataSet> model : chart.getDataModelList()) {
        final DataModel<LineChartDataSet> clonedModel =
            new DataModel<>(model.getTitle(), model.getColor());

        for (final LineChartDataSet dataSet : model.getDataSetList()) {
          // Use brighter color as this will be the 2nd dataset
          final Color color = ((User) model.getUserobObject()).getPlayerColor().brighterColor;
          clonedModel.addDataSet(
              new LineChartDataSet(
                  color, dataSet.getStroke(), dataSet.getLoops(), dataSet.getValues()));
        }

        clonedChart.addModel(clonedModel);
      }

      clonedChartList.add(clonedChart);
    }

    // Cloning done, let's roll on to calculating the FOOD_MADE data set...
    calculateDataSets(Stat.FOOD_MADE);

    // And the final step: "merge" the 2 data sets into one, simply add the datasets...
    for (int chartIdx = 0; chartIdx < chartList.size(); chartIdx++) {
      final Chart<LineChartDataSet> chart = chartList.get(chartIdx);
      final Chart<LineChartDataSet> clonedChart = clonedChartList.get(chartIdx);

      for (int modelIdx = 0; modelIdx < chart.getDataModelList().size(); modelIdx++) {
        final DataModel<LineChartDataSet> model = chart.getDataModelList().get(modelIdx);
        final DataModel<LineChartDataSet> clonedModel =
            clonedChart.getDataModelList().get(modelIdx);

        model.getDataSetList().addAll(clonedModel.getDataSetList());
      }
    }
  }

  /**
   * Calculates the data sets for the specified {@link Stat}.
   *
   * @param stat stat to calculate the data sets for
   */
  private void calculateDataSets(final Stat stat) {
    // Data points for each data set, inside being mapped from loop to value.
    final Map<LineChartDataSet, Map<Integer, Int>> dataSetPointsMap = new HashMap<>();

    final boolean mergeMinGas = stat.hasMinGas && !separateMinsGasCheckBox.isSelected();

    final String[] eventFields; // Event fields to query eventually
    if (stat.hasMinGas) {
      final boolean includeMins = minsCheckBox.isSelected();
      final boolean includeGas = gasCheckBox.isSelected();
      if (includeMins && includeGas) eventFields = stat.eventFields;
      else if (includeMins) eventFields = new String[] {stat.eventFields[0]};
      else if (includeGas) eventFields = new String[] {stat.eventFields[1]};
      else eventFields = new String[0];
    } else eventFields = stat.eventFields;

    // Calculate data points maps
    for (final Event event : repProc.replay.trackerEvents.events) {
      if (event.id != ITrackerEvents.ID_PLAYER_STATS) continue;

      final DataModel<LineChartDataSet> model = modelByPlayerIds[event.getPlayerId()];
      if (model == null) continue;

      // One extra player stat event is recorded when a user leaves (at the same loop) even if at
      // the same loop there was a
      // scheduled player stat event. So we have to exclude the last player stat event (to avoid the
      // chart getting
      // "screwed"). (This for example contains mineralsCurrent = 0.)
      // Note: if there's a scheduled stat event when the player left, this will be excluded
      // wrongly,
      // but I don't care, it won't make any noticable difference...
      if (event.loop == repProc.usersByPlayerId[event.getPlayerId()].leaveLoop) continue;

      // Data sets per event fields
      for (int i = 0; i < eventFields.length; i++) {
        final String field = eventFields[i];
        final LineChartDataSet dataSet = model.getDataSetList().get(mergeMinGas ? 0 : i);

        Map<Integer, Int> pointsMap = dataSetPointsMap.get(dataSet);
        if (pointsMap == null) dataSetPointsMap.put(dataSet, pointsMap = new HashMap<>());
        final Integer loop = Integer.valueOf(event.loop);
        Int point = pointsMap.get(loop);
        if (point == null) {
          pointsMap.put(loop, point = new Int());
        }
        int value = ((PlayerStatsEvent) event).stats.<Integer>get(field);
        if (field == PlayerStatsEvent.F_FOOD_MADE || field == PlayerStatsEvent.F_FOOD_USED)
          value >>= 12;
        point.value += value;
      }
    }

    // Convert data points maps to arrays
    for (final Chart<LineChartDataSet> chart : chartList) {
      for (final DataModel<LineChartDataSet> model : chart.getDataModelList()) {
        for (final LineChartDataSet dataSet : model.getDataSetList()) {
          final Map<Integer, Int> pointsMap = dataSetPointsMap.get(dataSet);
          if (pointsMap == null) continue;

          // Data points are needed sorted by loop
          final List<Entry<Integer, Int>> dataPointList = new ArrayList<>(pointsMap.entrySet());
          Collections.sort(
              dataPointList,
              new Comparator<Entry<Integer, Int>>() {
                @Override
                public int compare(Entry<Integer, Int> o1, Entry<Integer, Int> o2) {
                  return o1.getKey().compareTo(o2.getKey());
                }
              });

          int i = 0;
          final int[] loops = new int[pointsMap.size()];
          final int[] values = new int[pointsMap.size()];
          for (final Entry<Integer, Int> dataPoint : dataPointList) {
            loops[i] = dataPoint.getKey();
            values[i] = dataPoint.getValue().value;
            i++;
          }

          dataSet.setLoops(loops);
          dataSet.setValues(values);
          dataSet.calculateValueMax();
        }
      }
    }
  }

  @Override
  public void reconfigureChartCanvas() {
    final boolean gasOnly =
        statComboBox.getSelectedItem().hasMinGas
            && separateMinsGasCheckBox.isSelected()
            && !minsCheckBox.isSelected();

    final Presentation p = presentationComboBox.getSelectedItem();

    for (final Chart<?> chart : chartsComp.getChartsCanvas().getChartList()) {
      if (chart instanceof LineTimeChart) {
        final LineTimeChart lc = (LineTimeChart) chart;
        lc.setPresentation(p);

        // Stroke depends on the presentation:
        for (final DataModel<LineChartDataSet> model : lc.getDataModelList()) {
          int i = 0;
          for (final LineChartDataSet dataSet : model.getDataSetList())
            dataSet.setStroke(i++ > 0 || gasOnly ? p.storke : p.storkeDouble);
        }
      }
    }
  }
}
示例#2
0
  /**
   * Creates a new {@link BinaryDataComp}.
   *
   * @param repProc replay processor
   */
  public BinaryDataComp(final RepProcessor repProc) {
    super(repProc);

    hexLineSizeComboBox =
        SettingsGui.createSettingComboBox(
            Settings.HEX_LINE_SIZE, Env.APP_SETTINGS, rebuilderListener);

    final Vector<DataSource> sourceVector = new Vector<>();
    final List<DataSource> separatedDsList = new ArrayList<>();

    // The replay file
    sourceVector.add(new FullFileDataSource("Replay File", repProc.file));
    separatedDsList.add(sourceVector.get(sourceVector.size() - 1));
    // General Replay MPQ content
    sourceVector.add(new MpqUserDataDataSource(repProc.file));
    for (final MpqContent mpqContent : MpqContent.VALUES)
      sourceVector.add(new MpqContentDataSource(repProc.file, mpqContent));
    separatedDsList.add(sourceVector.get(sourceVector.size() - 1));
    // Replay content
    for (final RepContent mpqContent : RepContent.VALUES) {
      sourceVector.add(new MpqContentDataSource(repProc.file, mpqContent));
      if (mpqContent == RepContent.TRACKER_EVENTS)
        separatedDsList.add(sourceVector.get(sourceVector.size() - 1));
    }
    separatedDsList.add(sourceVector.get(sourceVector.size() - 1));
    // General Map MPQ content
    final Path mapFile = MapParser.getMapFile(repProc);
    sourceVector.add(new MpqUserDataDataSource(mapFile));
    for (final MpqContent mpqContent : MpqContent.VALUES)
      sourceVector.add(new MpqContentDataSource(mapFile, mpqContent));
    separatedDsList.add(sourceVector.get(sourceVector.size() - 1));
    // Map content
    for (final MapContent mpqContent : MapContent.VALUES)
      sourceVector.add(new MpqContentDataSource(mapFile, mpqContent));

    dataSourceComboBox = new XComboBox<>(sourceVector);
    dataSourceComboBox.markSeparatedItems(separatedDsList);

    searcher =
        new ByteIntPosBaseSearcher() {
          /** Bytes of the search text, as <code>int</code>s. */
          private int[] searchTextBytes;

          @Override
          protected void prepareNew() {
            // Note: Texts found in binary data are usually UTF-8 encoded.
            // This text search converts the searched text to bytes using the same
            // encoding (UTF-8), and this byte sequence will be searched.
            // For more details see the Tips.BINARY_DATA_TEXT_SEARCH tip.
            final byte[] bytes = searchText.getBytes(Env.UTF8);
            searchTextBytes = new int[bytes.length];
            maxOffset = searchTextBytes.length - 1;
            for (int i = maxOffset; i >= 0; i--) searchTextBytes[i] = bytes[i] & 0xff;
            super.prepareNew();
          }

          @Override
          public boolean matches() {
            // Local vars for fast access
            final byte[] data = BinaryDataComp.this.data;
            final int[] searchTextBytes = this.searchTextBytes;
            final int searchPos = this.searchPos;
            if (searchPos + maxOffset > maxPos)
              return false; // Would overflow in data, surely cannot match

            for (int offset = maxOffset; offset >= 0; offset--) {
              final int searchChar = searchTextBytes[offset];
              final int ch = data[searchPos + offset] & 0xff;
              if (ch != searchChar) {
                // Also check in a case insensitive manner (lower-cased version of ch):
                if (ch >= 'A' && ch <= 'Z') {
                  if (ch - ('A' - 'a') != searchChar) return false;
                } else return false;
              }
            }

            // Match!
            handleMatch();
            // Clear the other searcher (so it will start from this line)
            hexSearcher.clearLastSearchPos();

            return true;
          }
        };

    hexSearcher =
        new ByteIntPosBaseSearcher() {
          /** Bytes specified by the hex search text. */
          private byte[] searchTextBytes;

          @Override
          protected void prepareNew() {
            searchTextBytes = Utils.hexToBytes(searchText);
            if (searchTextBytes == null) searchTextBytes = new byte[0];
            maxOffset = searchTextBytes.length - 1;
            super.prepareNew();
          }

          @Override
          public boolean matches() {
            if (searchTextBytes.length == 0) return true; // To end the search
            // Local vars for fast access
            final byte[] data = BinaryDataComp.this.data;
            final byte[] searchTextBytes = this.searchTextBytes;
            final int searchPos = this.searchPos;
            if (searchPos + maxOffset > maxPos)
              return false; // Would overflow in data, surely cannot match

            for (int offset = maxOffset; offset >= 0; offset--)
              if (searchTextBytes[offset] != data[searchPos + offset]) return false;

            // Match!
            handleMatch();
            // Clear the other searcher (so it will start from this line)
            searcher.clearLastSearchPos();

            return true;
          }
        };

    buildGui();
  }