text: // Copyright 2014 The Chromium Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  #include "ui/display/manager/chromeos/touch_transform_controller.h"
  
  #include <utility>
  #include <vector>
  
  #include "third_party/skia/include/core/SkMatrix44.h"
  #include "ui/display/display_layout.h"
  #include "ui/display/manager/chromeos/display_configurator.h"
  #include "ui/display/manager/display_manager.h"
  #include "ui/display/manager/managed_display_info.h"
  #include "ui/display/screen.h"
  #include "ui/display/types/display_constants.h"
  #include "ui/display/types/display_snapshot.h"
  #include "ui/events/devices/device_data_manager.h"
  
  namespace display {
  
  namespace {
  
  ui::TouchscreenDevice FindTouchscreenById(int id) {
    const std::vector<ui::TouchscreenDevice>& touchscreens =
        ui::DeviceDataManager::GetInstance()->GetTouchscreenDevices();
    for (const auto& touchscreen : touchscreens) {
      if (touchscreen.id == id)
        return touchscreen;
    }
  
    return ui::TouchscreenDevice();
  }
  
  // Given an array of touch point and display point pairs, this function computes
  // and returns the constants(defined below) using a least fit algorithm.
  // If (xt, yt) is a touch point then its corresponding (xd, yd) would be defined
  // by the following 2 equations:
  // xd = xt * A + yt * B + C
  // yd = xt * D + yt * E + F
  // This function computes A, B, C, D, E and F and sets |ctm| with the calibrated
  // transform matrix. In case the computation fails, the function will return
  // false.
  // See http://crbug.com/672293
  bool GetCalibratedTransform(
      std::array<std::pair<gfx::Point, gfx::Point>, 4> touch_point_pairs,
      const gfx::Transform& pre_calibration_tm,
      gfx::Transform* ctm) {
    // Transform the display points before solving the equation.
    // If the calibration was performed at a resolution that is 0.5 times the
    // current resolution, then the display points (x, y) for a given touch point
    // now represents a display point at (2 * x, 2 * y). This and other kinds of
    // similar tranforms can be applied using |pre_calibration_tm|.
    for (int row = 0; row < 4; row++)
      pre_calibration_tm.TransformPoint(&touch_point_pairs[row].first);
  
    // Vector of the X-coordinate of display points corresponding to each of the
    // touch points.
    SkVector4 display_points_x(
        touch_point_pairs[0].first.x(), touch_point_pairs[1].first.x(),
        touch_point_pairs[2].first.x(), touch_point_pairs[3].first.x());
    // Vector of the Y-coordinate of display points corresponding to each of the
    // touch points.
    SkVector4 display_points_y(
        touch_point_pairs[0].first.y(), touch_point_pairs[1].first.y(),
        touch_point_pairs[2].first.y(), touch_point_pairs[3].first.y());
  
    // Initialize |touch_point_matrix|
    // If {(xt_1, yt_1), (xt_2, yt_2), (xt_3, yt_3)....} are a set of touch points
    // received during calibration, then the |touch_point_matrix| would be defined
    // as:
    // |xt_1  yt_1  1  0|
    // |xt_2  yt_2  1  0|
    // |xt_3  yt_3  1  0|
    // |xt_4  yt_4  1  0|
    SkMatrix44 touch_point_matrix;
    for (int row = 0; row < 4; row++) {
      touch_point_matrix.set(row, 0, touch_point_pairs[row].second.x());
      touch_point_matrix.set(row, 1, touch_point_pairs[row].second.y());
      touch_point_matrix.set(row, 2, 1);
      touch_point_matrix.set(row, 3, 0);
    }
    SkMatrix44 touch_point_matrix_transpose(touch_point_matrix);
    touch_point_matrix_transpose.transpose();
  
    SkMatrix44 product_matrix = touch_point_matrix_transpose * touch_point_matrix;
  
    // Set (3, 3) = 1 so that |determinent| of the matrix is != 0 and the inverse
    // can be calculated.
    product_matrix.set(3, 3, 1);
  
    SkMatrix44 product_matrix_inverse;
  
    // NOTE: If the determinent is zero then the inverse cannot be computed. The
    // only solution is to restart touch calibration and get new points from user.
    if (!product_matrix.invert(&product_matrix_inverse)) {
      NOTREACHED() << "Touch Calibration failed. Determinent is zero.";
      return false;
    }
  
    product_matrix_inverse.set(3, 3, 0);
  
    product_matrix = product_matrix_inverse * touch_point_matrix_transpose;
  
    // Constants [A, B, C, 0] used to calibrate the x-coordinate of touch input.
    // x_new = x_old * A + y_old * B + C;
    SkVector4 x_constants = product_matrix * display_points_x;
    // Constants [D, E, F, 0] used to calibrate the y-coordinate of touch input.
    // y_new = x_old * D + y_old * E + F;
    SkVector4 y_constants = product_matrix * display_points_y;
  
    // Create a transform matrix using the touch calibration data.
    ctm->ConcatTransform(gfx::Transform(
        x_constants.fData[0], x_constants.fData[1], 0, x_constants.fData[2],
        y_constants.fData[0], y_constants.fData[1], 0, y_constants.fData[2], 0, 0,
        1, 0, 0, 0, 0, 1));
    return true;
  }
  
  // Returns an uncalibrated touch transform.
  gfx::Transform GetUncalibratedTransform(const gfx::Transform& tm,
                                          const ManagedDisplayInfo& display,
                                          const ManagedDisplayInfo& touch_display,
                                          const gfx::SizeF& touch_area,
                                          const gfx::SizeF& touch_native_size) {
    gfx::SizeF current_size(display.bounds_in_native().size());
    gfx::Transform ctm(tm);
    // Take care of panel fitting only if supported. Panel fitting is emulated
    // in software mirroring mode (display != touch_display).
    // If panel fitting is enabled then the aspect ratio is preserved and the
    // display is scaled acordingly. In this case blank regions would be present
    // in order to center the displayed area.
    if (display.is_aspect_preserving_scaling() ||
        display.id() != touch_display.id()) {
      float touch_calib_ar =
          touch_native_size.width() / touch_native_size.height();
      float current_ar = current_size.width() / current_size.height();
  
      if (current_ar > touch_calib_ar) {  // Letterboxing
        ctm.Translate(
            0, (1 - current_ar / touch_calib_ar) * 0.5 * current_size.height());
        ctm.Scale(1, current_ar / touch_calib_ar);
      } else if (touch_calib_ar > current_ar) {  // Pillarboxing
        ctm.Translate(
            (1 - touch_calib_ar / current_ar) * 0.5 * current_size.width(), 0);
        ctm.Scale(touch_calib_ar / current_ar, 1);
      }
    }
    // Take care of scaling between touchscreen area and display resolution.
    ctm.Scale(current_size.width() / touch_area.width(),
              current_size.height() / touch_area.height());
    return ctm;
  }
  
  }  // namespace
  
  // This is to compute the scale ratio for the TouchEvent's radius. The
  // configured resolution of the display is not always the same as the touch
  // screen's reporting resolution, e.g. the display could be set as
  // 1920x1080 while the touchscreen is reporting touch position range at
  // 32767x32767. Touch radius is reported in the units the same as touch position
  // so we need to scale the touch radius to be compatible with the display's
  // resolution. We compute the scale as
  // sqrt of (display_area / touchscreen_area)
  double TouchTransformController::GetTouchResolutionScale(
      const ManagedDisplayInfo& touch_display,
      const ui::TouchscreenDevice& touch_device) const {
    if (touch_device.id == ui::InputDevice::kInvalidId ||
        touch_device.size.IsEmpty() ||
        touch_display.bounds_in_native().size().IsEmpty())
      return 1.0;
  
    double display_area = touch_display.bounds_in_native().size().GetArea();
    double touch_area = touch_device.size.GetArea();
    double ratio = std::sqrt(display_area / touch_area);
  
    VLOG(2) << "Display size: "
            << touch_display.bounds_in_native().size().ToString()
            << ", Touchscreen size: " << touch_device.size.ToString()
            << ", Touch radius scale ratio: " << ratio;
    return ratio;
  }
  
  gfx::Transform TouchTransformController::GetTouchTransform(
      const ManagedDisplayInfo& display,
      const ManagedDisplayInfo& touch_display,
      const ui::TouchscreenDevice& touchscreen,
      const gfx::Size& framebuffer_size) const {
    auto current_size = gfx::SizeF(display.bounds_in_native().size());
    auto touch_native_size = gfx::SizeF(touch_display.GetNativeModeSize());
  #if defined(USE_OZONE)
    auto touch_area = gfx::SizeF(touchscreen.size);
  #elif defined(USE_X11)
    // On X11 touches are reported in the framebuffer coordinate space.
    auto touch_area = gfx::SizeF(framebuffer_size);
  #endif
  
    gfx::Transform ctm;
  
    if (current_size.IsEmpty() || touch_native_size.IsEmpty() ||
        touch_area.IsEmpty() || touchscreen.id == ui::InputDevice::kInvalidId)
      return ctm;
  
  #if defined(USE_OZONE)
    // Translate the touch so that it falls within the display bounds. This
    // should not be performed if the displays are mirrored.
    if (display.id() == touch_display.id()) {
      ctm.Translate(display.bounds_in_native().x(),
                    display.bounds_in_native().y());
    }
  #endif
  
    // If the device is currently under calibration, then do not return any
    // transform as we want to use the raw native touch input data for calibration
    if (is_calibrating_)
      return ctm;
  
    // If touch calibration data is unavailable, use naive approach.
    if (!touch_display.has_touch_calibration_data()) {
      return GetUncalibratedTransform(ctm, display, touch_display, touch_area,
                                      touch_native_size);
    }
  
    // The resolution at which the touch calibration was performed.
    gfx::SizeF touch_calib_size(touch_display.GetTouchCalibrationData().bounds);
  
    // Any additional transfomration that needs to be applied to the display
    // points, before we solve for the final transform.
    gfx::Transform pre_transform;
  
    if (display.id() != touch_display.id() ||
        display.is_aspect_preserving_scaling()) {
      // Case of displays being mirrored or in panel fitting mode.
      // Aspect ratio of the touch display's resolution during calibration.
      float calib_ar = touch_calib_size.width() / touch_calib_size.height();
      // Aspect ratio of the display that is being mirrored.
      float current_ar = current_size.width() / current_size.height();
  
      if (current_ar < calib_ar) {
        pre_transform.Scale(current_size.height() / touch_calib_size.height(),
                            current_size.height() / touch_calib_size.height());
        pre_transform.Translate(
            (current_ar / calib_ar - 1.f) * touch_calib_size.width() * 0.5f, 0);
      } else {
        pre_transform.Scale(current_size.width() / touch_calib_size.width(),
                            current_size.width() / touch_calib_size.width());
        pre_transform.Translate(
            0, (calib_ar / current_ar - 1.f) * touch_calib_size.height() * 0.5f);
      }
    } else {
      // Case of current resolution being different from the resolution when the
      // touch calibration was performed.
      pre_transform.Scale(current_size.width() / touch_calib_size.width(),
                          current_size.height() / touch_calib_size.height());
    }
    // Solve for coefficients and compute transform matrix.
    gfx::Transform stored_ctm;
    if (!GetCalibratedTransform(
            touch_display.GetTouchCalibrationData().point_pairs, pre_transform,
            &stored_ctm)) {
      // TODO(malaykeshav): This can be checked at the calibration step before
      // storing the calibration associated data. This will allow us to explicitly
      // inform the user with proper UX.
  
      // Clear stored calibration data.
      display_manager_->ClearTouchCalibrationData(touch_display.id());
  
      // Return uncalibrated transform.
      return GetUncalibratedTransform(ctm, display, touch_display, touch_area,
                                      touch_native_size);
    }
  
    stored_ctm.ConcatTransform(ctm);
    return stored_ctm;
  }
  
  TouchTransformController::TouchTransformController(
      DisplayConfigurator* display_configurator,
      DisplayManager* display_manager)
      : display_configurator_(display_configurator),
        display_manager_(display_manager) {}
  
  TouchTransformController::~TouchTransformController() {}
  
  void TouchTransformController::UpdateTouchRadius(
      const ManagedDisplayInfo& display) const {
    ui::DeviceDataManager* device_manager = ui::DeviceDataManager::GetInstance();
    for (const auto& device_id : display.input_devices()) {
      device_manager->UpdateTouchRadiusScale(
          device_id,
          GetTouchResolutionScale(display, FindTouchscreenById(device_id)));
    }
  }
  
  void TouchTransformController::UpdateTouchTransform(
      int64_t target_display_id,
      const ManagedDisplayInfo& touch_display,
      const ManagedDisplayInfo& target_display) const {
    ui::DeviceDataManager* device_manager = ui::DeviceDataManager::GetInstance();
    gfx::Size fb_size = display_configurator_->framebuffer_size();
    for (const auto& device_id : touch_display.input_devices()) {
      device_manager->UpdateTouchInfoForDisplay(
          target_display_id, device_id,
          GetTouchTransform(target_display, touch_display,
                            FindTouchscreenById(device_id), fb_size));
    }
  }
  
  void TouchTransformController::UpdateTouchTransforms() const {
    ui::DeviceDataManager::GetInstance()->ClearTouchDeviceAssociations();
  
    // Display IDs and ManagedDisplayInfo for mirror or extended mode.
    int64_t display1_id = kInvalidDisplayId;
    int64_t display2_id = kInvalidDisplayId;
    ManagedDisplayInfo display1;
    ManagedDisplayInfo display2;
    // Display ID and ManagedDisplayInfo for single display mode.
    int64_t single_display_id = kInvalidDisplayId;
    ManagedDisplayInfo single_display;
  
    if (display_manager_->num_connected_displays() == 0) {
      return;
    } else if (display_manager_->num_connected_displays() == 1 ||
               display_manager_->IsInUnifiedMode()) {
      single_display_id = display_manager_->first_display_id();
      DCHECK(single_display_id != kInvalidDisplayId);
      single_display = display_manager_->GetDisplayInfo(single_display_id);
      UpdateTouchRadius(single_display);
    } else {
      DisplayIdList list = display_manager_->GetCurrentDisplayIdList();
      display1_id = list[0];
      display2_id = list[1];
      DCHECK(display1_id != kInvalidDisplayId &&
             display2_id != kInvalidDisplayId);
      display1 = display_manager_->GetDisplayInfo(display1_id);
      display2 = display_manager_->GetDisplayInfo(display2_id);
      UpdateTouchRadius(display1);
      UpdateTouchRadius(display2);
    }
  
    if (display_manager_->IsInMirrorMode()) {
      int64_t primary_display_id = Screen::GetScreen()->GetPrimaryDisplay().id();
      if (display_manager_->SoftwareMirroringEnabled()) {
        // In extended but software mirroring mode, there is a WindowTreeHost for
        // each display, but all touches are forwarded to the primary root
        // window's WindowTreeHost.
        ManagedDisplayInfo target_display =
            primary_display_id == display1_id ? display1 : display2;
        UpdateTouchTransform(target_display.id(), display1, target_display);
        UpdateTouchTransform(target_display.id(), display2, target_display);
      } else {
        // In mirror mode, there is just one WindowTreeHost and two displays. Make
        // the WindowTreeHost accept touch events from both displays.
        UpdateTouchTransform(primary_display_id, display1, display1);
        UpdateTouchTransform(primary_display_id, display2, display2);
      }
      return;
    }
  
    if (display_manager_->num_connected_displays() > 1) {
      // In actual extended mode, each display is associated with one
      // WindowTreeHost.
      UpdateTouchTransform(display1_id, display1, display1);
      UpdateTouchTransform(display2_id, display2, display2);
      return;
    }
  
    // Single display mode. The WindowTreeHost has one associated display id.
    UpdateTouchTransform(single_display_id, single_display, single_display);
  }
  
  void TouchTransformController::SetForCalibration(bool is_calibrating) {
    is_calibrating_ = is_calibrating;
    UpdateTouchTransforms();
  }
  
  }  // namespace display
  