Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
ScreenHelper.h 6.38 KiB
#pragma once

#include <QRect>              // QRect
#include <QPoint>             // QPoint
#include "settings/PanZoomState.h"
#include "math.h"

/**
 * Screen helper functions
 */
namespace BioTracker {
namespace Core {
namespace ScreenHelper {

/**
 * @brief calculate_viewport
 * @param im_w width of the image
 * @param im_h height of the image
 * @param w width of the videoview
 * @param h height of the videoview
 * @param window OUT parameter
 * @param viewport OUT parameter
 */
inline float calculate_viewport(
    const int im_w, const int im_h,
    const int w, const int h,
    QRect &window, QRect &viewport) {

    // We use setWindow and setViewport to fit the video into the
    // given video widget frame (with width "w" and height "h")
    // we later need to adjust an offset caused the use of different
    // dimensions for window and viewport.
    const float im_wf = static_cast<float>(im_w);
    const float im_hf = static_cast<float>(im_h);
    const float w_f = static_cast<float>(w);
    const float h_f = static_cast<float>(h);

    float ratio = 0;
    int offset_top = 0;
    int offset_left = 0;

    if ((im_wf/im_hf) > (w_f/h_f)) {
        // image ratio is bigger (wider image than window)
        ratio = im_wf / w_f;
        const float px = w_f / im_wf;
        const float px_im_hf = px * im_hf;
        offset_top = h/2 - static_cast<int>(px_im_hf / 2);
    } else {
        // window ratio is bigger (narrow image)
        ratio = im_hf / h_f;
        const float px = h_f / im_hf;
        const float px_im_wf = px * im_wf;
        offset_left = w/2 - static_cast<int>(px_im_wf / 2);
    }

    const int screen_w = static_cast<int>(im_wf * ratio);
    const int screen_h = static_cast<int>(im_hf * ratio);

    window.setX(0);
    window.setY(0);
    window.setWidth(screen_w);
    window.setHeight(screen_h);

    viewport.setX(offset_left);
    viewport.setY(offset_top);
    viewport.setWidth(im_w);
    viewport.setHeight(im_h);

    // adjust the panning as the viewport is potentially scewed
    // and mouse movements given by the window are not translated
    // one-to-one anymore
    return screen_w / im_wf;
}

/**
 * @brief getImDimsInScreenCoords
 * Calculates the actual dimension of the image with all zoom, pan and
 * viewport transformations applied
 * @return a rectangle that represents the position and dimension of the
 * image in the videoview element
 */
inline QRect getImDimsInScreenCoords(
    const PanZoomState zoomState,
    const int im_w, const int im_h,
    const int w, const int h) {

    QRect viewport, window;
    const float viewportRatio = calculate_viewport(im_w, im_h, w, h, window, viewport);
    const float zoom = 1 + zoomState.zoomFactor;
    // back-translate the pan to non-zoomed coordinate space
    float realPanX = -zoomState.panX;
    float realPanY = -zoomState.panY;
    realPanX += (viewport.x());
    realPanY += (viewport.y());

    // The image in screen coords
    QRect actualIm(
        static_cast<int>(realPanX), static_cast<int>(realPanY),
        static_cast<int>((im_w / viewportRatio) * zoom),
        static_cast<int>((im_h / viewportRatio) * zoom)
    );
    return actualIm;
}

/**
 * @brief imageToScreenCoords
 */
inline QPoint imageToScreenCoords(
    const PanZoomState zoomState,
    const int im_w, const int im_h,
    const int w, const int h,
    const QPoint poi) {
    QPoint result;
    const QRect actualIm = getImDimsInScreenCoords(zoomState, im_w, im_h, w, h);
    const float one_step_x = actualIm.width() / static_cast<float>(im_w);
    const float one_step_y = actualIm.height() / static_cast<float>(im_h);
    const int actualPosXInIm = static_cast<int>(round(poi.x() * one_step_x));
    const int actualPosYInIm = static_cast<int>(round(poi.y() * one_step_y));
    result.setX(actualPosXInIm + actualIm.x());
    result.setY(actualPosYInIm + actualIm.y());
    return result;
}

/**
 * @brief screenToImageCoords
 * Translate the window coordinates to the actual image coords
 * @param im_w image width
 * @param im_h image height
 * @param w width of the videoview
 * @param h height of the videoview
 * @param poi Point in Screen Coords
 */
inline QPoint screenToImageCoords(
    const BioTracker::Core::PanZoomState zoomState,
    const int im_w, const int im_h,
    const int w, const int h,
    QPoint poi) {
    QPoint result(0, 0);

    // The image in screen coords
    QRect actualIm = getImDimsInScreenCoords(zoomState, im_w, im_h, w, h);

    const float im_wf = static_cast<float>(im_w);
    const float im_hf = static_cast<float>(im_h);
    const float one_step_x = im_wf / actualIm.width();
    const float one_step_y = im_hf / actualIm.height();

    const float transformedX = (-actualIm.x() + poi.x()) * one_step_x;
    const float transformedY = (-actualIm.y() + poi.y()) * one_step_y;

    result.setX(static_cast<int>(round(transformedX)));
    result.setY(static_cast<int>(round(transformedY)));

    return result;
}

/**
 * @brief zoomTo
 * Modifies the panzoomstate so that the cursor (zoomCenter) will point to the same
 * location in the image as prior to the zoom
 * @param im_w width of the image (in px)
 * @param im_h height of the image (in px)
 * @param w width of the videoview element
 * @param h height of the videoview element
 */
inline PanZoomState zoomTo(
    PanZoomState state,
    const int im_w,
    const int im_h,
    const int w,
    const int h,
    const float deltaZoom,
    const QPoint zoomCenter) {

    const QPoint imPos = screenToImageCoords(state, im_w, im_h, w, h, zoomCenter);

    const float oldZoomFactor = state.zoomFactor;
    const float newZoomFactor = state.zoomFactor - (deltaZoom/2000);

    if (newZoomFactor <= -1.0f) {
        // MAX VALUE
        return state;
    }

    const float zoom = 1 + newZoomFactor;

    const float oldPanX = state.panX / (1 + oldZoomFactor);
    const float oldPanY = state.panY / (1 + oldZoomFactor);

    // zoom with origin in (0/0)
    state.panX = oldPanX * zoom;
    state.panY = oldPanY * zoom;
    state.zoomFactor = newZoomFactor;

    // As we do not want to zoom to (0/0) but rather keep the focus on the chosen
    // image position (zoomCenter) we now have to "back-translate" to our initial
    // position
    const QPoint translatedZoomCenter = imageToScreenCoords(state, im_w, im_h, w, h, imPos);
    const QPoint translate = translatedZoomCenter - zoomCenter;
    state.panX += translate.x();
    state.panY += translate.y();
    state.isChanged = true;
    return state;
}

}
}
}