aboutsummaryrefslogtreecommitdiff
#include "lineplot.h"

#include <algorithm>
#include <cmath>
#include <limits>

#include <QQuickWindow>
#include <QOpenGLFunctions>
#include <QOpenGLShaderProgram>
#include <QOpenGLVertexArrayObject>
#include <QSGGeometryNode>
#include <QSGSimpleRectNode>

// From CUBu
#include "cpubundling.h"
#include "graph.h"

#include "continuouscolorscale.h"
#include "geometry.h"
#include "scatterplot.h"

static const float EPSILON = 1e-5f;
static const int SAMPLES = 128;

LinePlot::LinePlot(QQuickItem *parent)
    : QQuickFramebufferObject(parent)
    , m_sx(0, 1, 0, 1)
    , m_sy(0, 1, 0, 1)
    , m_brushedItem(-1)
    , m_anySelected(false)
    , m_linesChanged(false)
    , m_valuesChanged(false)
    , m_colorScaleChanged(false)
    , m_updateOffsets(false)

    // Q_PROPERTY's
    , m_iterations(15)
    , m_kernelSize(32)
    , m_smoothingFactor(0.2)
    , m_smoothingIterations(1)

    , m_blockEndpoints(true)
    , m_endsIterations(0)
    , m_endsKernelSize(32)
    , m_endsSmoothingFactor(0.5)

    , m_edgeSampling(15)
    , m_advectionSpeed(0.5)
    , m_relaxation(0)
    , m_bundleGPU(true)
    , m_lineWidth(2.0)
{
    setFlag(QQuickItem::ItemHasContents);
    setTextureFollowsItemSize(false);
}

void LinePlot::setColorScale(const ColorScale *scale)
{
    m_cmap.resize(SAMPLES * 3);
    scale->sample(SAMPLES, m_cmap.begin());

    setColorScaleChanged(true);
    update();
}

void LinePlot::relax()
{
    m_gdFinal = m_gdBundle;
    m_gdFinal.interpolate(*m_gdPtr.get(), m_relaxation);

    setLinesChanged(true);
}

void LinePlot::buildGraph()
{
    Graph g(m_Y.n_rows);
    PointSet points;

    m_sx.setRange(0, width());
    m_sy.setRange(0, height());
    for (arma::uword i = 0; i < m_Y.n_rows; i++) {
        points.push_back(Point2d(m_sx(m_Y(i, 0)), m_sy(m_Y(i, 1))));
    }

    for (arma::uword k = 0; k < m_indices.n_elem; k += 2) {
        arma::uword i = m_indices[k + 0],
                    j = m_indices[k + 1];
        g(i, j) = g(j, i) = m_values[k];
    }

    m_gdPtr.reset(new GraphDrawing);
    m_gdPtr.get()->build(&g, &points);
}

void LinePlot::bundle()
{
    m_gdBundle = *m_gdPtr.get();

    CPUBundling bundling(std::min(width(), height()));
    bundling.setInput(&m_gdBundle);

    bundling.niter  = m_iterations;
    bundling.h      = m_kernelSize;
    bundling.lambda = m_smoothingFactor;
    bundling.liter  = m_smoothingIterations;

    bundling.block_endpoints = m_blockEndpoints;
    bundling.niter_ms        = m_endsIterations;
    bundling.h_ms            = m_endsKernelSize;
    bundling.lambda_ends     = m_endsSmoothingFactor;

    bundling.spl = m_edgeSampling;
    bundling.eps = m_advectionSpeed;

    if (m_bundleGPU) {
        bundling.bundleGPU();
    } else {
        bundling.bundleCPU();
    }
    m_updateOffsets = true;

    relax();
}

void LinePlot::setLines(const arma::uvec &indices, const arma::mat &Y)
{
    if (indices.n_elem % 2 != 0 || Y.n_cols != 2) {
        return;
    }

    m_indices = indices;
    emit indicesChanged(m_indices);

    m_Y = Y;
    emit pointsChanged(m_Y);

    // Clear current selection
    m_anySelected = false;
    m_selection.assign(m_Y.n_rows, false);
    emit selectionChanged(m_selection);

    // Build the line plot's internal representation: a graph where each
    // endpoint of a line is a node, each line is a link...
    buildGraph();

    // ... then bundle the edges
    bundle();

    update();
}

void LinePlot::setValues(const arma::vec &values)
{
    m_values.resize(values.n_elem);
    std::transform(values.begin(), values.end(), m_values.begin(), [](float v) {
        return std::max(v, EPSILON);
    });
    emit valuesChanged(values);

    setValuesChanged(true);
    update();
}

void LinePlot::setScale(const LinearScale<float> &sx,
                        const LinearScale<float> &sy)
{
    m_sx = sx;
    m_sy = sy;
    emit scaleChanged(m_sx, m_sy);
    update();
}

void LinePlot::brushItem(int item)
{
    m_brushedItem = item;
    m_updateOffsets = true;
    update();
}

void LinePlot::setSelection(const std::vector<bool> &selection)
{
    m_selection = selection;
    m_anySelected = std::any_of(m_selection.cbegin(),
                                m_selection.cend(),
                                [](bool b) { return b; });
    emit selectionChanged(m_selection);

    // XXX: *possibly* needed; doesn't seem to make much of a difference
    //bundle();
    m_updateOffsets = true;
    update();
}

// Q_PROPERTY's
void LinePlot::setIterations(int iterations) {
    if (m_iterations == iterations) {
        return;
    }

    m_iterations = iterations;
    emit iterationsChanged(m_iterations);

    bundle();
    update();
}

void LinePlot::setKernelSize(float kernelSize)
{
    if (m_kernelSize == kernelSize) {
        return;
    }

    m_kernelSize = kernelSize;
    emit kernelSizeChanged(m_kernelSize);

    bundle();
    update();
}

void LinePlot::setSmoothingFactor(float smoothingFactor)
{
    if (m_smoothingFactor == smoothingFactor) {
        return;
    }

    m_smoothingFactor = smoothingFactor;
    emit smoothingFactorChanged(m_smoothingFactor);

    bundle();
    update();
}

void LinePlot::setSmoothingIterations(int smoothingIterations)
{
    if (m_smoothingIterations == smoothingIterations) {
        return;
    }

    m_smoothingIterations = smoothingIterations;
    emit smoothingIterationsChanged(m_smoothingIterations);

    bundle();
    update();
}

void LinePlot::setBlockEndpoints(bool blockEndpoints)
{
    if (m_blockEndpoints == blockEndpoints) {
        return;
    }

    m_blockEndpoints = blockEndpoints;
    emit blockEndpointsChanged(m_blockEndpoints);

    bundle();
    update();
}

void LinePlot::setEndsIterations(int endsIterations)
{
    if (m_endsIterations == endsIterations) {
        return;
    }

    m_endsIterations = endsIterations;
    emit endsIterationsChanged(m_endsIterations);

    bundle();
    update();
}

void LinePlot::setEndsKernelSize(float endsKernelSize)
{
    if (m_endsKernelSize == endsKernelSize) {
        return;
    }

    m_endsKernelSize = endsKernelSize;
    emit endsKernelSizeChanged(m_endsKernelSize);

    bundle();
    update();
}

void LinePlot::setEndsSmoothingFactor(float endsSmoothingFactor)
{
    if (m_endsSmoothingFactor == endsSmoothingFactor) {
        return;
    }

    m_endsSmoothingFactor = endsSmoothingFactor;
    emit endsSmoothingFactorChanged(m_endsSmoothingFactor);

    bundle();
    update();
}

void LinePlot::setEdgeSampling(float edgeSampling)
{
    if (m_edgeSampling == edgeSampling) {
        return;
    }

    m_edgeSampling = edgeSampling;
    emit edgeSamplingChanged(m_edgeSampling);

    bundle();
    update();
}

void LinePlot::setAdvectionSpeed(float advectionSpeed)
{
    if (m_advectionSpeed == advectionSpeed) {
        return;
    }

    m_advectionSpeed = advectionSpeed;
    emit advectionSpeedChanged(m_advectionSpeed);

    bundle();
    update();
}

void LinePlot::setRelaxation(float relaxation)
{
    if (m_relaxation == relaxation) {
        return;
    }

    m_relaxation = relaxation;
    emit relaxationChanged(m_relaxation);

    relax();
    update();
}

void LinePlot::setBundleGPU(bool bundleGPU)
{
    if (m_bundleGPU == bundleGPU) {
        return;
    }

    m_bundleGPU = bundleGPU;
    emit bundleGPUChanged(m_bundleGPU);

    bundle();
    update();
}

void LinePlot::setLineWidth(float lineWidth)
{
    if (m_lineWidth == lineWidth) {
        return;
    }

    m_lineWidth = lineWidth;
    emit lineWidthChanged(m_lineWidth);

    update();
}

// ----------------------------------------------------------------------------

class LinePlotRenderer
    : public QQuickFramebufferObject::Renderer
{
public:
    LinePlotRenderer();
    virtual ~LinePlotRenderer();

protected:
    QOpenGLFramebufferObject *createFramebufferObject(const QSize &size);
    void render();
    void synchronize(QQuickFramebufferObject *item);

private:
    void setupShaders();
    void setupVAOs();
    void setupTextures();

    void updatePoints();
    void updateValues();
    void updateColormap();

    void copyPolylines(const LinePlot *plot);
    void computeOffsets(const LinePlot *plot);
    void computeBrushOffsets(const LinePlot *plot);
    void computeAllOffsets(const LinePlot *plot);

    QSize m_size;
    std::vector<float> m_points;
    const std::vector<float> *m_values, *m_cmap;
    float m_lineWidth;
    std::vector<int> m_offsets;

    QQuickWindow *m_window; // used to reset OpenGL state (as per docs)
    QOpenGLFunctions gl;
    QOpenGLShaderProgram *m_program;
    GLuint m_VBOs[2], m_colormapTex;
    QOpenGLVertexArrayObject m_VAO;
    GLfloat m_transform[4][4];
    LinearScale<float> m_sx, m_sy;
    bool m_pointsChanged, m_valuesChanged, m_colormapChanged;
};

QQuickFramebufferObject::Renderer *LinePlot::createRenderer() const
{
    return new LinePlotRenderer;
}

LinePlotRenderer::LinePlotRenderer()
    : gl(QOpenGLContext::currentContext())
    , m_sx(0.0f, 1.0f, 0.0f, 1.0f)
    , m_sy(0.0f, 1.0f, 0.0f, 1.0f)
    , m_pointsChanged(false)
    , m_valuesChanged(false)
    , m_colormapChanged(false)
{
    std::fill(&m_transform[0][0], &m_transform[0][0] + 16, 0.0f);
    m_transform[3][3] = 1.0f;
    setupShaders();
    setupVAOs();
    setupTextures();
}

void LinePlotRenderer::setupShaders()
{
    m_program = new QOpenGLShaderProgram;
    m_program->addShaderFromSourceCode(QOpenGLShader::Vertex,
R"EOF(#version 440

uniform mat4 transform;

in vec2 vert;
in float scalar;

out float value;

void main() {
  gl_Position = transform * vec4(vert, 0.0, 1.0);
  value = scalar;
}
)EOF");
    m_program->addShaderFromSourceCode(QOpenGLShader::Fragment,
R"EOF(#version 440

uniform sampler2D colormap;
in float value;

layout (location = 0) out vec4 fragColor;

vec3 getRGB(float value) {
  return texture(colormap, vec2(mix(0.005, 0.995, value), 0)).rgb;
}

void main() {
  fragColor = vec4(getRGB(value), 1.0);
}
)EOF");
    m_program->link();
}

void LinePlotRenderer::setupVAOs()
{
    gl.glGenBuffers(2, m_VBOs);

    m_VAO.create();
    m_VAO.bind();
    gl.glBindBuffer(GL_ARRAY_BUFFER, m_VBOs[0]);
    int vertAttrib = m_program->attributeLocation("vert");
    gl.glVertexAttribPointer(vertAttrib, 2, GL_FLOAT, GL_FALSE, 0, 0);
    gl.glEnableVertexAttribArray(vertAttrib);

    gl.glBindBuffer(GL_ARRAY_BUFFER, m_VBOs[1]);
    int valueAttrib = m_program->attributeLocation("scalar");
    gl.glVertexAttribPointer(valueAttrib, 1, GL_FLOAT, GL_FALSE, 0, 0);
    gl.glEnableVertexAttribArray(valueAttrib);
    m_VAO.release();
}

void LinePlotRenderer::setupTextures()
{
    gl.glGenTextures(1, &m_colormapTex);
    gl.glBindTexture(GL_TEXTURE_2D, m_colormapTex);
    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
}

LinePlotRenderer::~LinePlotRenderer()
{
    gl.glDeleteBuffers(2, m_VBOs);
    gl.glDeleteTextures(1, &m_colormapTex);

    delete m_program;
}

QOpenGLFramebufferObject *LinePlotRenderer::createFramebufferObject(const QSize &size)
{
    m_size = size;
    GLfloat w = m_size.width(), h = m_size.height();

    GLfloat rangeOffset = Scatterplot::PADDING / w;
    m_sx.setDomain(0, w);
    m_sx.setRange(rangeOffset, 1.0f - rangeOffset);
    GLfloat sx = 2.0f * m_sx.slope();
    GLfloat tx = 2.0f * m_sx.offset() - 1.0f;

    rangeOffset = Scatterplot::PADDING / h;
    m_sy.setDomain(0, h);
    m_sy.setRange(1.0f - rangeOffset, rangeOffset); // inverted on purpose
    GLfloat sy = 2.0f * m_sy.slope();
    GLfloat ty = 2.0f * m_sy.offset() - 1.0f;

    // The transform matrix should be this (but transposed -- column major):
    // [   sx  0.0f  0.0f    tx ]
    // [ 0.0f    sy  0.0f    ty ]
    // [ 0.0f  0.0f  0.0f  0.0f ]
    // [ 0.0f  0.0f  0.0f  1.0f ]
    m_transform[0][0] = sx;
    m_transform[1][1] = sy;
    m_transform[3][0] = tx;
    m_transform[3][1] = ty;

    return QQuickFramebufferObject::Renderer::createFramebufferObject(m_size);
}

void LinePlotRenderer::render()
{
    // Update OpenGL buffers and textures as needed
    if (m_pointsChanged) {
        updatePoints();
    }
    if (m_valuesChanged) {
        updateValues();
    }
    if (m_colormapChanged) {
        updateColormap();
    }

    m_program->bind();

    gl.glActiveTexture(GL_TEXTURE0);
    gl.glBindTexture(GL_TEXTURE_2D, m_colormapTex);
    m_program->setUniformValue("colormap", 0);
    m_program->setUniformValue("transform", m_transform);

    gl.glClearColor(0, 0, 0, 0);
    gl.glClear(GL_COLOR_BUFFER_BIT);

    m_VAO.bind();
    gl.glEnable(GL_LINE_SMOOTH);
    gl.glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
    gl.glEnable(GL_BLEND);
    gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    gl.glLineWidth(m_lineWidth);
    for (int i = 0; i < m_offsets.size(); i += 2) {
        gl.glDrawArrays(GL_LINE_STRIP, m_offsets[i], m_offsets[i + 1]);
    }
    gl.glDisable(GL_LINE_SMOOTH);
    gl.glDisable(GL_BLEND);
    m_VAO.release();

    m_program->release();
    m_window->resetOpenGLState();
}

void LinePlotRenderer::synchronize(QQuickFramebufferObject *item)
{
    LinePlot *plot = static_cast<LinePlot *>(item);

    m_pointsChanged   = plot->m_linesChanged;
    m_valuesChanged   = plot->m_valuesChanged;
    m_colormapChanged = plot->m_colorScaleChanged;

    m_values    = &(plot->values());
    m_cmap      = &(plot->colorScale());
    m_lineWidth = plot->m_lineWidth;
    m_window    = plot->window();

    if (plot->m_updateOffsets) {
        plot->m_updateOffsets = false;
        computeOffsets(plot);
    }

    if (m_pointsChanged) {
        copyPolylines(plot);
    }

    plot->m_linesChanged      = false;
    plot->m_valuesChanged     = false;
    plot->m_colorScaleChanged = false;
}

void LinePlotRenderer::computeOffsets(const LinePlot *plot)
{
    if (plot->m_brushedItem >= 0) {
        computeBrushOffsets(plot);
    } else {
        computeAllOffsets(plot);
    }
}

void LinePlotRenderer::computeBrushOffsets(const LinePlot *plot)
{
    int offset = 0;
    m_offsets.clear();
    for (auto &p : plot->graphDrawing()->draw_order) {
        int pointsNum = p.second->size();
        //int item1 = plot->m_indices[2*i];
        //int item2 = plot->m_indices[2*i + 1];
        //if (item1 == plot->m_brushedItem
        //    || item2 == plot->m_brushedItem) {
            m_offsets.push_back(offset);
            m_offsets.push_back(pointsNum);
        //}

        offset += pointsNum;
    }
}

void LinePlotRenderer::computeAllOffsets(const LinePlot *plot)
{
    int offset = 0;
    m_offsets.clear();
    m_offsets.reserve(2 * plot->graphDrawing()->draw_order.size());
    for (auto &p : plot->graphDrawing()->draw_order) {
        int pointsNum = p.second->size();
        m_offsets.push_back(offset);
        m_offsets.push_back(pointsNum);

        offset += pointsNum;
    }
}

void LinePlotRenderer::copyPolylines(const LinePlot *plot)
{
    const GraphDrawing *gd = plot->graphDrawing();
    if (!gd || gd->draw_order.empty()) {
        return;
    }

    m_points.clear();
    //m_points.reserve(2 * totalPoints);
    for (auto &p : gd->draw_order) {
        for (auto &point : *p.second) {
            m_points.push_back(point.x);
            m_points.push_back(point.y);
        }
    }
}

void LinePlotRenderer::updatePoints()
{
    gl.glBindBuffer(GL_ARRAY_BUFFER, m_VBOs[0]);
    gl.glBufferData(GL_ARRAY_BUFFER, m_points.size() * sizeof(float),
            m_points.data(), GL_DYNAMIC_DRAW);

    m_pointsChanged = false;
}

void LinePlotRenderer::updateValues()
{
    gl.glBindBuffer(GL_ARRAY_BUFFER, m_VBOs[1]);
    gl.glBufferData(GL_ARRAY_BUFFER, m_values->size() * sizeof(float),
            m_values->data(), GL_DYNAMIC_DRAW);

    m_valuesChanged = false;
}

void LinePlotRenderer::updateColormap()
{
    gl.glBindTexture(GL_TEXTURE_2D, m_colormapTex);
    gl.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, m_cmap->size() / 3, 1, 0, GL_RGB,
            GL_FLOAT, m_cmap->data());

    m_colormapChanged = false;
}