////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2001-2026 Alexey Kuryakin daqgroup@mail.ru under MIT license //
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// This file is part of the CRW-DAQ project by DaqGroup - component CRWLIB.   //
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// Purpose:                                                                   //
// Largest-Triangle-Three Bucket DownSampling implementation:                 //
// Reference:                                                                 //
//  https://github.com/sveinn-steinarsson/highcharts-downsample               //
//  https://gist.github.com/adrianseeley/264417d295ccd006e7fd                 //
//  https://www.codearteng.com/2020/08/implementation-of-downsampling.html    //
//  https://skemman.is/handle/1946/15343                                      //
//  https://skemman.is/bitstream/1946/15343/3/SS_MSthesis.pdf                 //
//  https://www.codeproject.com/Articles/5370403/                             //
//          Implementation-Downsampling-Algorithm-in-MSChart-E                //
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// History:                                                                   //
// 20231026 - Created by A.K.                                                 //
// 20231028 - 1st release                                                     //
// 20231129 - fix LTTB bug; add Tail; add LTOB method                         //
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// The MIT License
//
// Copyright (c) 2013 by Sveinn Steinarsson
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// Modified (very much) 2023 by Alex Kuryakin  for Object Pascal
//
/////////////////////////////////////////////////////////////////////////////////

unit _crw_lttb; // Largest Triangle Three Buckets DownSampling

{$I _crw_sysdef.inc}

{$I _crw_sysmode.inc}

{$WARN 5023 off : Unit "$1" not used in $2}

interface

uses
 //////////////////////////////////////////////////////
 {$I _crw_uses_first.inc} // NB: MUST BE FIRST USES !!!
 //////////////////////////////////////////////////////
 sysutils, classes, math,
 _crw_alloc, _crw_fpu, _crw_str, _crw_fio, _crw_ee, _crw_fifo,
 _crw_plut, _crw_curves;

const          // DownSampling Methods:
 dsm_NONE = 0; // None, Don't use DownSampling
 dsm_LTTB = 1; // Largest Triangle Three Buckets
 dsm_LTOB = 2; // Largest Triangle One Bucket
 dsm_MIDD = 3; // Middle point of bucket
 dsm_MEAN = 4; // Mean value over bucket
 dsm_GOOD = [dsm_LTTB,dsm_LTOB,dsm_MIDD,dsm_MEAN]; // Implemented DownSampling methods
 dsm_LIST = 'None'+EOL
           +'LargestTriangleThreeBuckets'+EOL
           +'LargestTriangleOneBucket'+EOL
           +'MiddleOfBucket'+EOL
           +'MeanOverBucket';

const            // DownSampling Mode flags:
 dsm_Enable = 1; // DownSampling is enabled
 dsm_AbsLen = 2; // Uses AbsLen for DownSampling
 dsm_PerPix = 4; // Uses PerPix for DownSampling
 dsm_Tail   = 8; // Uses Tail   for DownSampling

type  // DownSampling parameters
 TDownSamplingParams = packed record
  Method : Integer;  // one of dsm_XXXX methods
  Mode   : Integer;  // Bit0: Use DownSampling; Bit1: Use AbsLen, Bit2: Use PerPix
  AbsLen : Integer;  // Absolute Length  = expected number of points after DownSampling
  PerPix : Double;   // Points per pixel = expected num. points per pixel after DownSampling
  Tail   : Integer;  // Protected Tail   = number of tail ponts keep unchanged ("as is")
 end;

const // Default DownSampling parameters
 DefDownSamplingParams : TDownSamplingParams =
 (Method:dsm_LTTB; Mode:15; AbsLen:4096; PerPix:2; Tail:3);

 /////////////////////////////////////////////////////////////////////////////////
 // Apply downsampling algorithm for (Data) curve to have less (about DownLeng) //
 // samples to plot data (much) faster.                                         //
 // The curve (Data) assumed to have X axis data in increasing order.           //
 // Arguments:                                                                  //
 //  1) Data     - curve contains data points to downsample.                    //
 //  2) DownLeng - number of points expected after downsampling.                //
 //  3) Method   - method how to make downsampling, see dsm_XXXX constants.     //
 // Return:                                                                     //
 //  1) Original Data curve pointer if downsampling is not applicable.          //
 //     For example, if (DownLeng<4) or (DownLeng>Data.Count).                  //
 //  2) New sampled curve pointer with (DataLength<=DownLeng),                  //
 //     if downsampling is succeeded.                                           //
 // NB:                                                                         //
 //  Be careful to avoid free memory twice!                                     //
 //  The function can return original (Data) curve on downsampling failed,      //
 //  or return new curve pointer on downsampling succeeded.                     //
 //  So you MUST check result pointer before free curve.                        //
 /////////////////////////////////////////////////////////////////////////////////
function CurveDownSampling(Data:TCurve; DownLeng:LongInt; Method:Integer=-1; Tail:Integer=-1):TCurve;
function LargestTriangleThreeBucketsDownSampling(Data:TCurve; DownLeng:LongInt; Tail:Integer):TCurve;
function LargestTriangleOneBucketDownSampling(Data:TCurve; DownLeng:LongInt; Tail:Integer):TCurve;
function MiddleOfBucketDownSampling(Data:TCurve; DownLeng:LongInt; Tail:Integer):TCurve;
function MeanOverBucketDownSampling(Data:TCurve; DownLeng:LongInt; Tail:Integer):TCurve;

 // Read DownSampling params from INI file like Crw32.ini [System] DefDownSamplingParams = 1 15 4096 2 3
function ReadIniFileDownSampling(IniFile,Section,ParName:LongString; var Par:TDownSamplingParams):Boolean;

 // LTTB testing function, just for demo.
function Test_LTTB(DataLeng:Integer=14; DownLeng:Integer=6):LongString;

implementation

 /////////////////////////////
 // Internal service routines.
 /////////////////////////////

 // Add sample point (P) to curve (sampled) at index (sampled_index) and increment index.
procedure AddSample(sampled:TCurve; var sampled_index:LongInt; const P:TPoint2D);
begin
 sampled[sampled_index]:=P;
 Inc(sampled_index);
end;

 // Find the bucket end index by right boundary (Bound).
 // Bucket end index is equals to first index of next bucket.
 // Local optimization: use direct data access PX[i] instead of Data[i].X.
procedure FindBucketEnd(Data:TCurve; var Index:LongInt; DataLeng:LongInt; const Bound:Double);
var PX:PDoubleArray;
begin
 PX:=Data.PX;                                                // Optimization: use PX[i] instead of Data[i].X
 while (Index<DataLeng) and (PX[Index]<Bound) do Inc(Index); // Find the bucket end index
 if (Index>DataLeng) then Index:=DataLeng;                   // Prevent index overrun
end;

 // Find average (mean) value (avg) of bucket Data[data_beg..data_end-1] & return bucket length (span).
function BucketMeanPoint(const Data:TCurve; const data_beg,data_end:LongInt; out avg:TPoint2D):LongInt;
var PX,PY:PDoubleArray; span,b:LongInt; point_b:TPoint2D;
begin
 PX:=Data.PX; PY:=Data.PY;                // Optimization: direct data access PX[i] instead of Data[i].X
 avg.x:=0; avg.y:=0;                      // avg = average = mean value
 span:=data_end-data_beg;                 // Span = number of data points in bucket
 if (span>0) then begin                   // Skip empty bucket to avoid div by zero
  for b:=data_beg to data_end-1 do begin  // For each data point in bucket:
   point_b.x:=PX[b]; point_b.y:=PY[b];    // Fetch point (b)
   avg.x:=avg.x+point_b.x;                // Sum X values
   avg.y:=avg.y+point_b.y;                // Sum Y values
  end;                                    //
  avg.x:=avg.x/span;                      // Average X = Mean X Value in bucket
  avg.y:=avg.y/span;                      // Average Y = Mean Y Value in bucket
 end;                                     //
 if IsNanOrInf(avg) then span:=0;         // Avoid invalid data
 Result:=span;                            // Return number of points in bucket
end;

 // Find middle value (mid) of the bucket Data[data_beg..data_end-1] and return bucket length (span).
function BucketMiddlePoint(const Data:TCurve; const data_beg,data_end:LongInt; out mid:TPoint2D):LongInt;
var PX,PY:PDoubleArray; span,b:LongInt; point_b:TPoint2D;
begin
 PX:=Data.PX; PY:=Data.PY;                // Optimization: direct data access PX[i] instead of Data[i].X
 mid.x:=0; mid.y:=0;                      // mid = middle value
 span:=data_end-data_beg;                 // Span = number of data points in bucket
 if (span>0) then begin                   // Skip empty bucket to avoid invalid data
  b:=data_beg+(span shr 1);               // Middle point b
  point_b.x:=PX[b]; point_b.y:=PY[b];     // Fetch point (b)
  mid:=point_b;                           // Return as result
 end;                                     //
 if IsNanOrInf(mid) then span:=0;         // Avoid invalid data
 Result:=span;                            // Return number of points in bucket
end;

  // Calculate triangle area over 3 points (a,b,c).
function TriangleArea(const a,b,c:TPoint2D):Double; {$IFDEF FPC}inline;{$ENDIF}
begin
 Result:=abs((a.x-c.x)*(b.y-a.y)-(a.x-b.x)*(c.y-a.y))*0.5;
end;

// Largest-Triangle-Three-Buckets
// Return index (b) of point in bucket Data[buck_beg,buck_end-1] with maximal (a,b,c) triangle area.
// Point b is current point in bucket, a is previous bucket point, c is next bucket mean point.
function Find_LTTB(Data:TCurve; buck_beg,buck_end:LongInt; const point_a,point_c:TPoint2D):Integer;
var PX,PY:PDoubleArray; b:LongInt; point_b:TPoint2D; area,max_area:Double;
begin
 PX:=Data.PX; PY:=Data.PY;                     // Optimization: direct data access PX[i] instead of Data[i].X
 Result:=-1; max_area:=-1;                     // Initialize maximal area seach variables
 for b:=buck_beg to buck_end-1 do begin        // Calculate triangle area over three buckets
  point_b.x:=PX[b]; point_b.y:=PY[b];          // Point b = current bucket data point to test
  area:=TriangleArea(point_a,point_b,point_c); // Calculate (a,b,c) triangle area over three buckets
  if (area>max_area) then begin                // Find point b of triangle (a,b,c)
   max_area:=area;                             // with maximal area
   Result:=b;                                  // Result is this b.
  end;                                         //
 end;                                          //
end;

 // Largest-Triangle-One-Bucket
 // Return index (b) of point in bucket Data[buck_beg,buck_end-1] with maximal (a,b,c) triangle area.
 // Point b is current point in bucket, a is previous neighbor point, c is next neighbor point.
function Find_LTOB(Data:TCurve; buck_beg,buck_end:LongInt):Integer;
var PX,PY:PDoubleArray; a,b,c:LongInt; point_a,point_b,point_c:TPoint2D; area,max_area:Double;
begin
 PX:=Data.PX; PY:=Data.PY;                     // Optimization: direct data access PX[i] instead of Data[i].X
 Result:=-1; max_area:=-1;                     // Initialize maximal area seach variables
 for b:=buck_beg to buck_end-1 do begin        // Calculate triangle area over three points
  a:=b-1; c:=b+1;                              // Previous(a), current(b) and next(c) point
  point_a.x:=PX[a]; point_a.y:=PY[a];          // Point a = prev data point
  point_b.x:=PX[b]; point_b.y:=PY[b];          // Point b = curr data point to test
  point_c.x:=PX[c]; point_c.y:=PY[c];          // Point c = next data point
  area:=TriangleArea(point_a,point_b,point_c); // Calculate (a,b,c) triangle area over three points
  if (area>max_area) then begin                // Find point b of triangle (a,b,c)
   max_area:=area;                             // with maximal effective area
   Result:=b;                                  // Result is this b.
  end;                                         //
 end;                                          //
end;

 // Prepare and check downsampling parameters (DataLeng,DownLeng,Tail).
 // The Tail uses to keep Tail right endpoints unchanged (useful for online data acquisition).
function PrepareDsParams(Data:TCurve; out DataLeng:LongInt; var DownLeng,Tail:Integer):Boolean;
begin
 if (Tail<0) then Tail:=Max(0,DefDownSamplingParams.Tail);      // Protected tail on right side
 if (Tail>0) then DownLeng:=DownLeng-Tail;                      // Keep Tail endponts unchanged
 DataLeng:=Data.Count-Tail;                                     // Get data length = number of points except tail
 Result:=(DataLeng>DownLeng) and (DownLeng>=3);                 // Check it's valid for downsampling
end;

 // Calculate first bucket start (xBeg) and bucket size (bSizeX) for bucket boundary.
procedure BucketStartSize(Data:TCurve; DataLeng,DownLeng:LongInt; out xBeg,bSizeX:Double);
var x0,x1,x2,x3,xEnd:Double;
begin
 x0:=Data[0].x;                    // First two points
 x1:=Data[1].x;                    //       
 x2:=Data[DataLeng-2].x;           // Last  two points
 x3:=Data[DataLeng-1].x;           //       
 xBeg:=x0+(x1-x0)*0.5;             // Mid X point for first 2 points = begin point of buckets
 xEnd:=x2+(x3-x2)*0.5;             // Mid X point for last  2 points = end   point of buckets
 bSizeX:=(xEnd-xBeg)/(DownLeng-2); // Data bucket size X, with excluded first and last points
end;

 // Add tail Data[DataLeng..DataLeng+Tail-1] points to sampled curve.
procedure AddTailPoints(sampled,Data:TCurve; DataLeng,Tail:LongInt);
begin
 if (Tail>0) and (DataLeng>0) and (DataLeng+Tail<=Data.Count)
 then sampled.AddPoints(Data.PX[DataLeng],Data.PY[DataLeng],Tail);
end;

 /////////////////////////////
 // Primary curve DownSampling
 /////////////////////////////

function CurveDownSampling(Data:TCurve; DownLeng:LongInt; Method:Integer=-1; Tail:Integer=-1):TCurve;
begin
 if (Method<0) then Method:=DefDownSamplingParams.Method;
 case Method of
  dsm_LTTB: Result:=LargestTriangleThreeBucketsDownSampling(Data,DownLeng,Tail);
  dsm_LTOB: Result:=LargestTriangleOneBucketDownSampling(Data,DownLeng,Tail);
  dsm_MIDD: Result:=MiddleOfBucketDownSampling(Data,DownLeng,Tail);
  dsm_MEAN: Result:=MeanOverBucketDownSampling(Data,DownLeng,Tail);
  else      Result:=Data;
 end;
end;

 /////////////////////////////////////////////////////////////////////
 // Based on https://skemman.is/bitstream/1946/15343/3/SS_MSthesis.pdf
 /////////////////////////////////////////////////////////////////////
function LargestTriangleThreeBucketsDownSampling(Data:TCurve; DownLeng:LongInt; Tail:Integer):TCurve;
var DataLeng,sampled_index,a,b,nc,ib,data_beg,data_end,buck_beg,buck_end:LongInt;
var sampled:TCurve; point_a,point_b,point_c:TPoint2D;
var xBeg,bSizeX,bBound:Double;
begin
 Result:=Data;                                                    // By default or on fail return Data
 if Assigned(Data) then                                           // Check Data curve is valid
 try                                                              //
  Data.Lock;                                                      // Lock Data for thread safety
  try                                                             //
   if not PrepareDsParams(Data,DataLeng,DownLeng,Tail) then Exit; // Prepare & check downsampling parameters
   sampled:=NewCurve(DownLeng,data.Name,Data.Color,Data.Style);   // Curve for (down)sampled data
   try                                                            //
    a:=0;                                                         // a is first point in triangle
    sampled_index:=0;                                             // Index for (down)sampled points
    point_b:=Point2D(0,0);                                        //
    AddSample(sampled,sampled_index,Data[a]);                     // Always add the first point
    BucketStartSize(Data,DataLeng,DownLeng,xBeg,bSizeX);          // Calculate bucket start & size
    bBound:=xBeg+bSizeX;                                          // Right boundary of data bucket
    data_beg:=0; buck_beg:=1; buck_end:=buck_beg;                 // Bucket points begin/end index
    FindBucketEnd(Data,buck_end,DataLeng,bBound);                 // Find end index of curr bucket
    for ib:=0 to (DownLeng-2)-1 do begin                          // Loop for each bucket:
     FindBucketEnd(Data,data_beg,DataLeng,bBound);                // Start index of next bucket
     bBound:=bBound+bSizeX; data_end:=data_beg;                   // Move to next bucket boundary
     FindBucketEnd(Data,data_end,DataLeng,bBound);                // Find end index of next bucket
     point_a:=Data[a];                                            // Point a = reference data point of prev bucket
     nc:=BucketMeanPoint(Data,data_beg,data_end,point_c);         // Point c = average (mean) value of next bucket
     if (nc>0)                                                    // If next bucket is valid then use LTTB else LTOB
     then b:=Find_LTTB(Data,buck_beg,buck_end,point_a,point_c)    // Point b = point with maximal area triangle - LTTB
     else b:=Find_LTOB(Data,buck_beg,buck_end);                   // Point b = point with maximal area triangle - LTOB
     if (b>a) then point_b:=Data[b] else b:=a;                    // Negative b means fail: invalid bucket area
     if (b>a) then AddSample(sampled,sampled_index,point_b);      // Pick the point b from the bucket
     a:=b;                                                        // Next a is this b
     buck_beg:=data_beg; buck_end:=data_end; data_beg:=data_end;  // Move to next bucket
    end;                                                          //
    AddSample(sampled,sampled_index,Data[DataLeng-1]);            // Always add last point
    if (sampled_index>DownLeng) then Exit;                        // Invalid sampled length
    if (sampled.Count>sampled_index)                              // Resize array in case
    then sampled.Count:=sampled_index;                            // total length is less
    AddTailPoints(sampled,Data,DataLeng,Tail);                    // Add Tail data points to result
    Result:=sampled; sampled:=nil;                                // Return sampled curve as result
   finally                                                        //
    if Assigned(sampled) then Kill(sampled);                      // Kill sampled if something goes wrong
   end;                                                           //
  finally                                                         //
   Data.UnLock;                                                   // Don't forget to unlock Data
  end;                                                            //
 except
  on E:Exception do BugReport(E,nil,'LargestTriangleThreeBucketsDownSampling');
 end;
end;

 ////////////////////////////////////////////////////////////////////////
 // Use middle point of bucket: easy, fast but not nice (just for tests).
 ////////////////////////////////////////////////////////////////////////
function MiddleOfBucketDownSampling(Data:TCurve; DownLeng:LongInt; Tail:Integer):TCurve;
var DataLeng,sampled_index,a,ia,na,buck_beg,buck_end:LongInt;
var sampled:TCurve; point_a:TPoint2D;
var xBeg,bSizeX,bBound:Double;
begin
 Result:=Data;                                                    // By default or on fail return Data
 if Assigned(Data) then                                           // Check Data curve is valid
 try                                                              //
  Data.Lock;                                                      // Lock Data for thread safety
  try                                                             //
   if not PrepareDsParams(Data,DataLeng,DownLeng,Tail) then Exit; // Prepare & check downsampling parameters
   sampled:=NewCurve(DownLeng,data.Name,Data.Color,Data.Style);   // Curve for (down)sampled data
   try                                                            //
    a:=0;                                                         // a is first point
    sampled_index:=0;                                             // Index for sampled points
    AddSample(sampled,sampled_index,Data[a]);                     // Always add the first point
    BucketStartSize(Data,DataLeng,DownLeng,xBeg,bSizeX);          // Calculate bucket start & size
    bBound:=xBeg+bSizeX;                                          // Right boundary of data bucket
    buck_beg:=1; buck_end:=buck_beg;                              // Bucket points begin/end index
    for ia:=0 to (DownLeng-2)-1 do begin                          // Loop for each bucket:
     FindBucketEnd(Data,buck_end,DataLeng,bBound);                // Find end index of curr bucket
     bBound:=bBound+bSizeX;                                       // Right boundary of next bucket
     na:=BucketMiddlePoint(Data,buck_beg,buck_end,point_a);       // Point a = middle point bucket
     if (na>0) then AddSample(sampled,sampled_index,point_a);     // Add sample point a
     buck_beg:=buck_end;                                          // Move to next bucket
    end;                                                          //
    AddSample(sampled,sampled_index,Data[DataLeng-1]);            // Always add last point
    if (sampled_index>DownLeng) then Exit;                        // Invalid sampled length
    if (sampled.Count>sampled_index)                              // Resize array in case
    then sampled.Count:=sampled_index;                            // total length is less
    AddTailPoints(sampled,Data,DataLeng,Tail);                    // Add Tail data points to result
    Result:=sampled; sampled:=nil;                                // Return sampled curve as result
   finally                                                        //
    if Assigned(sampled) then Kill(sampled);                      // Kill sampled if something goes wrong
   end;                                                           //
  finally                                                         //
   Data.UnLock;                                                   // Don't forget to unlock Data
  end;                                                            //
 except
  on E:Exception do BugReport(E,nil,'MiddleOfBucketDownSampling');
 end;
end;

///////////////////////////////////////////////////////////////////////////
// Use mean point of bucket: easy, fast but not very nice (just for tests).
///////////////////////////////////////////////////////////////////////////
function MeanOverBucketDownSampling(Data:TCurve; DownLeng:LongInt; Tail:Integer):TCurve;
var DataLeng,sampled_index,a,ia,na,buck_beg,buck_end:LongInt;
var sampled:TCurve; point_a:TPoint2D;
var xBeg,bSizeX,bBound:Double;
begin
 Result:=Data;                                                    // By default or on fail return Data
 if Assigned(Data) then                                           // Check Data curve is valid
 try                                                              //
  Data.Lock;                                                      // Lock Data for thread safety
  try                                                             //
   if not PrepareDsParams(Data,DataLeng,DownLeng,Tail) then Exit; // Prepare & check downsampling parameters
   sampled:=NewCurve(DownLeng,data.Name,Data.Color,Data.Style);   // Curve for (down)sampled data
   try                                                            //
    a:=0;                                                         // a is first point
    sampled_index:=0;                                             // Index for sampled points
    AddSample(sampled,sampled_index,Data[a]);                     // Always add the first point
    BucketStartSize(Data,DataLeng,DownLeng,xBeg,bSizeX);          // Calculate bucket start & size
    bBound:=xBeg+bSizeX;                                          // Right boundary of data bucket
    buck_beg:=1; buck_end:=buck_beg;                              // Bucket points begin/end index
    for ia:=0 to (DownLeng-2)-1 do begin                          // Loop for each bucket:
     FindBucketEnd(Data,buck_end,DataLeng,bBound);                // Find end index of curr bucket
     bBound:=bBound+bSizeX;                                       // Right boundary of next bucket
     na:=BucketMeanPoint(Data,buck_beg,buck_end,point_a);         // Point a = mean value of bucket
     if (na>0) then AddSample(sampled,sampled_index,point_a);     // Add sample point a
     buck_beg:=buck_end;                                          // Move to next bucket
    end;                                                          //
    AddSample(sampled,sampled_index,Data[DataLeng-1]);            // Always add last point
    if (sampled_index>DownLeng) then Exit;                        // Invalid sampled length
    if (sampled.Count>sampled_index)                              // Resize array in case
    then sampled.Count:=sampled_index;                            // total length is less
    AddTailPoints(sampled,Data,DataLeng,Tail);                    // Add Tail data points to result
    Result:=sampled; sampled:=nil;                                // Return sampled curve as result
   finally                                                        //
    if Assigned(sampled) then Kill(sampled);                      // Kill sampled if something goes wrong
   end;                                                           //
  finally                                                         //
   Data.UnLock;                                                   // Don't forget to unlock Data
  end;                                                            //
 except
  on E:Exception do BugReport(E,nil,'MeanOverBucketDownSampling');
 end;
end;

 ///////////////////////////////////////////////////////////////////////////
 // Use LargestTriangleOneBucket: easy, fast but less nice (just for tests).
 ///////////////////////////////////////////////////////////////////////////
function LargestTriangleOneBucketDownSampling(Data:TCurve; DownLeng:LongInt; Tail:Integer):TCurve;
var DataLeng,sampled_index,a,ia,buck_beg,buck_end:LongInt;
var sampled:TCurve; point_a:TPoint2D;
var xBeg,bSizeX,bBound:Double;
begin
 Result:=Data;                                                    // By default or on fail return Data
 if Assigned(Data) then                                           // Check Data curve is valid
 try                                                              //
  Data.Lock;                                                      // Lock Data for thread safety
  try                                                             //
   if not PrepareDsParams(Data,DataLeng,DownLeng,Tail) then Exit; // Prepare & check downsampling parameters
   sampled:=NewCurve(DownLeng,data.Name,Data.Color,Data.Style);   // Curve for (down)sampled data
   try                                                            //
    a:=0;                                                         // a is first point
    sampled_index:=0;                                             // Index for sampled points
    AddSample(sampled,sampled_index,Data[a]);                     // Always add the first point
    BucketStartSize(Data,DataLeng,DownLeng,xBeg,bSizeX);          // Calculate bucket start & size
    bBound:=xBeg+bSizeX;                                          // Right boundary of data bucket
    buck_beg:=1; buck_end:=buck_beg;                              // Bucket points begin/end index
    for ia:=0 to (DownLeng-2)-1 do begin                          // Loop for each bucket:
     FindBucketEnd(Data,buck_end,DataLeng,bBound);                // Find end index of curr bucket
     bBound:=bBound+bSizeX;                                       // Right boundary of next bucket
     a:=Find_LTOB(Data,buck_beg,buck_end);                        // Find LargestTriangleOneBucket
     if (a>0) then point_a:=Data[a];                              // Point a = LTOB over bucket point
     if (a>0) then AddSample(sampled,sampled_index,point_a);      // Add sample point a
     buck_beg:=buck_end;                                          // Move to next bucket
    end;                                                          //
    AddSample(sampled,sampled_index,Data[DataLeng-1]);            // Always add last point
    if (sampled_index>DownLeng) then Exit;                        // Invalid sampled length
    if (sampled.Count>sampled_index)                              // Resize array in case
    then sampled.Count:=sampled_index;                            // total length is less
    AddTailPoints(sampled,Data,DataLeng,Tail);                    // Add Tail data points to result
    Result:=sampled; sampled:=nil;                                // Return sampled curve as result
   finally                                                        //
    if Assigned(sampled) then Kill(sampled);                      // Kill sampled if something goes wrong
   end;                                                           //
  finally                                                         //
   Data.UnLock;                                                   // Don't forget to unlock Data
  end;                                                            //
 except
  on E:Exception do BugReport(E,nil,'LargestTriangleOneBucketDownSampling');
 end;
end;

function ReadIniFileDownSampling(IniFile,Section,ParName:LongString; var Par:TDownSamplingParams):Boolean;
begin
 Result:=false;
 if IsNonEmptyStr(IniFile) then
 if IsNonEmptyStr(Section) then
 if IsNonEmptyStr(ParName) then
 Result:=ReadIniFileRecord(IniFile,Section,Trim(ParName)+'%i;%i;%i;%f;%i',Par);
end;

function Test_LTTB(DataLeng:Integer=14; DownLeng:Integer=6):LongString;
var Lines:TStringList; crvData,crvSamp:TCurve; i:Integer;
 procedure AddCurve(Caption:LongString; crv:TCurve);
 var i:Integer;
 begin
   Lines.Add(Caption);
   for i:=0 to crv.Count-1 do Lines.Add(Format('Data[%d] = %.5g, %.5g%',[i,crv[i].x,crv[i].y]));
 end;
begin
 Result:='';
 try
  crvData:=nil;
  crvSamp:=nil;
  Lines:=TStringList.Create;
  try
   crvData:=NewCurve(DataLeng,'Data');
   for i:=0 to DataLeng-1 do crvData[i]:=Point2D(i,sin(i)+0.1*(random-0.5));
   crvSamp:=LargestTriangleThreeBucketsDownSampling(crvData,DownLeng,0);
   AddCurve('DataCurve:',crvData);
   AddCurve('Sampled:',crvSamp);
   Result:=Lines.Text;
  finally
   if (crvSamp=crvData) then crvSamp:=nil;
   Kill(crvData); Kill(crvSamp);
   Lines.Free;
  end;
 except
  on E:Exception do BugReport(E,nil,'Test_LTTB');
 end;
end;

///////////////////////////////////////
// Unit initialization and finalization
///////////////////////////////////////

procedure Init_crw_lttb;
begin
end;

procedure Free_crw_lttb;
begin
end;

initialization

 Init_crw_lttb;

finalization

 Free_crw_lttb;

end.

//////////////
// END OF FILE
//////////////

