public DisplayPlus(
      final ImageCache stitchedCache,
      AcquisitionEngine eng,
      JSONObject summaryMD,
      boolean invX,
      boolean invY,
      boolean swapXY) {
    eng_ = eng;
    invertX_ = invX;
    invertY_ = invY;
    swapXY_ = swapXY;
    try {
      MultiStagePosition pos0 = MMStudioMainFrame.getInstance().getPositionList().getPosition(0);
      pos0x_ = pos0.getX();
      pos0y_ = pos0.getY();
      tileWidth_ = MDUtils.getHeight(summaryMD);
      tileHeight_ = MDUtils.getWidth(summaryMD);
    } catch (Exception e) {
      ReportingUtils.showError("Couldnt get grid info");
    }
    vad_ =
        new VirtualAcquisitionDisplay(stitchedCache, eng, WINDOW_TITLE) {
          public void showImage(final JSONObject tags, boolean waitForDisplay)
              throws InterruptedException, InvocationTargetException {
            // Since this is multichannel camera, only show when last channel arrives
            try {
              if (MDUtils.getChannelIndex(tags) == super.getNumChannels() - 1) {
                super.showImage(tags, waitForDisplay);
              } else {
                ImagePlus ip = super.getHyperImage();
                if (ip != null) {
                  // canvas never gets painted so need to set painpending false
                  ip.getCanvas().setPaintPending(false);
                }
              }
            } catch (JSONException ex) {
            }
          }
        };
    DisplayControls controls = new Controls();

    // Add in custom controls
    try {
      JavaUtils.setRestrictedFieldValue(
          vad_, VirtualAcquisitionDisplay.class, "controls_", controls);
    } catch (NoSuchFieldException ex) {
      ReportingUtils.showError("Couldn't create display controls");
    }
    vad_.show();
    // Zoom to 100%
    vad_.getImagePlus().getWindow().getCanvas().unzoom();

    // add mouse listeners for moving grids
    addMouseListeners();

    stitchedCache.addImageCacheListener(this);
  }
    private void updateLabels(JSONObject tags) {
      // Z position label
      String zPosition = "";
      try {
        zPosition = NumberUtils.doubleStringCoreToDisplay(tags.getString("ZPositionUm"));
      } catch (Exception e) {
        try {
          zPosition = NumberUtils.doubleStringCoreToDisplay(tags.getString("Z-um"));
        } catch (Exception e1) {
          // Do nothing...
        }
      }
      zPosLabel_.setText("Z Position: " + zPosition + " um        ");

      // time label
      try {
        int ms = (int) tags.getDouble("ElapsedTime-ms");
        int s = ms / 1000;
        int min = s / 60;
        int h = min / 60;

        String time =
            twoDigitFormat(h)
                + ":"
                + twoDigitFormat(min % 60)
                + ":"
                + twoDigitFormat(s % 60)
                + "."
                + threeDigitFormat(ms % 1000);
        timeStampLabel_.setText("Elapsed time: " + time + "      ");
      } catch (JSONException ex) {
        ReportingUtils.logError("MetaData did not contain ElapsedTime-ms field");
      }
    }
    @Override
    public void newImageUpdate(JSONObject tags) {
      if (tags == null) {
        return;
      }
      updateLabels(tags);
      try {
        if (vad_.acquisitionIsRunning() && vad_.getNextWakeTime() > 0) {
          final long nextImageTime = vad_.getNextWakeTime();
          if (System.nanoTime() / 1000000 < nextImageTime) {
            final java.util.Timer timer = new java.util.Timer("Next frame display");
            TimerTask task =
                new TimerTask() {

                  public void run() {
                    double timeRemainingS = (nextImageTime - System.nanoTime() / 1000000) / 1000;
                    if (timeRemainingS > 0 && vad_.acquisitionIsRunning()) {
                      setStatusLabel(
                          "Next frame: "
                              + NumberUtils.doubleToDisplayString(1 + timeRemainingS)
                              + " s");
                    } else {
                      timer.cancel();
                      setStatusLabel("");
                    }
                  }
                };
            timer.schedule(task, 2000, 100);
          }
        }

      } catch (Exception ex) {
        ReportingUtils.logError(ex);
      }
    }
  @Override
  public void imageReceived(TaggedImage taggedImage) {
    try {
      // duplicate so image storage doesnt see incorrect tags
      JSONObject newTags = new JSONObject(taggedImage.tags.toString());
      MDUtils.setPositionIndex(newTags, 0);
      taggedImage = new TaggedImage(taggedImage.pix, newTags);
    } catch (JSONException ex) {
      ReportingUtils.showError("Couldn't manipulate image tags for display");
    }

    vad_.imageReceived(taggedImage);
  }