How to store MinMax aggregate values as QualifiedValues in ProcessedHistoryColumn?

Continuing the discussion from Expected return format for MinMax aggregate using a Custom History Provider?:

I am trying to display both a minimum and maximum data for each timestamp in a table. I am adding both Minimum and Maximum values to ProcessedHistoryColumn, but the table only shows the first instance of the value.

How can I get both timestamps to appear for Minimum and Maximum?

I figured it out. I had to create my own History Node using decompiled versions of DefaultHistoryColumn and ProcessedHistoryColumn. ProcessedHistoryColumn does not work for MinMax and cannot be used.

I created the following column to be used for MinMax and the standard case of 1 value per column.


import com.inductiveautomation.ignition.common.TypeUtilities;
import com.inductiveautomation.ignition.common.model.values.BasicQualifiedValue;
import com.inductiveautomation.ignition.common.model.values.QualifiedValue;
import com.inductiveautomation.ignition.common.model.values.QualityCode;
import com.inductiveautomation.ignition.gateway.sqltags.history.query.AbstractHistoryNode;
import com.inductiveautomation.ignition.gateway.sqltags.history.query.HistoricalValue;
import com.inductiveautomation.ignition.gateway.sqltags.history.query.columns.HistoryColumn;

import java.util.*;

public class ProcessedIndexedHistoryColumn extends AbstractHistoryNode implements HistoryColumn {
    private LinkedList<QV>[] values;
    private QV[] lockedCurrentValues;
    private QV[] carryThroughValues;
    private int lastLevel;

    public ProcessedIndexedHistoryColumn(String name) {
        super(name);
        setNextAvailableTime(Long.MAX_VALUE);
    }

    @Override
    public void process(HistoricalValue historicalValue) {
        put(List.of(new BasicQualifiedValue(historicalValue.getValue(), historicalValue.getQuality(), new Date(historicalValue.getTimestamp()))));
    }

    @Override
    public void markCompleted(long time) {
        this.find(time, true, lastLevel);
        this.setNextAvailableTime(this.getNextTimestamp());
    }

    public long getNextTimestamp() {
        return this.values[0].isEmpty() ? Long.MAX_VALUE : this.values[0].peek().timestamp;
    }

    @Override
    public Object getValue(long timestamp, int index) {
        lastLevel = index;
        this.lockedCurrentValues[index] = this.find(timestamp, false, index);
        return this.lockedCurrentValues[index] == null ? null : TypeUtilities.coerce(this.lockedCurrentValues[index].value, this.type.getJavaType());
    }

    public void put(Collection<Collection<QualifiedValue>> aggregateSet) {
        if (!aggregateSet.isEmpty()) {
            this.values = new LinkedList[aggregateSet.size()];
            lockedCurrentValues = new QV[aggregateSet.size()];
            carryThroughValues = new QV[aggregateSet.size()];
            int i = 0;
            long startTimestamp = Long.MAX_VALUE;
            for (var aggregate : aggregateSet){
                this.values[i] = new LinkedList<>();
                for (var value : aggregate){
                    var qv = new QV(value);
                    if (qv.timestamp < startTimestamp){
                        startTimestamp = qv.timestamp;
                    }

                    this.values[i].add(qv);
                }
                i++;
            }

            this.setNextAvailableTime(startTimestamp);
        }
    }

    public void put(List<QualifiedValue> values) {
        if (!values.isEmpty()) {
            this.values = new LinkedList[1];
            this.values[0] = new LinkedList<>();
            lockedCurrentValues = new QV[1];
            carryThroughValues = new QV[1];
            for(QualifiedValue qv : values) {
                this.values[0].add(new QV(qv));
            }

            this.setNextAvailableTime((this.values[0].getFirst()).timestamp);
        }
    }

    @Override
    public QualityCode getQuality() {
        if (lockedCurrentValues.length > 0){
            return this.lockedCurrentValues[lastLevel].quality;
        }
        else{
            return QualityCode.Bad_NotFound;
        }
    }

    @Override
    public boolean hasMore() {
        return this.values != null && this.values.length - 1 > this.lastLevel;
    }

    @Override
    public boolean wasValueInterpolated() {
        return false;
    }

    protected QV find(long timestamp, boolean including, int index) {
        while(true) {
            label24: {
                if (values.length > 0){
                    if (!this.values[index].isEmpty()) {
                        var value = this.values[index].peek();
                        if (including){
                            if (value != null && value.timestamp <= timestamp){
                                break label24;
                            }
                        }
                        else{
                            if (value != null && value.timestamp < timestamp) {
                                break label24;
                            }
                        }
                    }
                }

                return this.values[index].isEmpty() ? this.carryThroughValues[index] : this.values[index].peek();
            }

            this.carryThroughValues[index] = this.values[index].pop();
        }
    }

    protected static class QV {
        public Object value;
        public QualityCode quality;
        public long timestamp;

        public QV(QualifiedValue qv) {
            this.value = qv.getValue();
            this.quality = qv.getQuality();
            this.timestamp = qv.getTimestamp().getTime();
        }
    }
}