<commit_before>/*
 * MultiLineClusterTracker.java
 *
 * Created on December 5, 2005, 3:49 AM
 *
 * To change this template, choose Tools | Options and locate the template under
 * the Source Creation and Management node. Right-click the template and choose
 * Open. You can then make changes to the template in the Source Editor.
 */

package ch.unizh.ini.caviar.eventprocessing.tracking;
import ch.unizh.ini.caviar.aemonitor.AEConstants;
import ch.unizh.ini.caviar.chip.*;
import ch.unizh.ini.caviar.eventprocessing.EventFilter2D;
import ch.unizh.ini.caviar.event.*;
import ch.unizh.ini.caviar.event.EventPacket;
import ch.unizh.ini.caviar.eventprocessing.TimeLimiter;
import ch.unizh.ini.caviar.graphics.*;
import com.sun.opengl.util.*;
import java.awt.*;
//import ch.unizh.ini.caviar.util.PreferencesEditor;
import java.awt.geom.*;
import java.io.*;
import java.util.*;
import java.util.prefs.*;
import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCanvas;
import javax.media.opengl.GLCapabilities;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.glu.GLU;
import javax.swing.*;

/**
 * Tracks multiple lines in the scene using a cluster based method based on pairs of recent events.
 * The event pairs come from a buffer formed from recent events. Each pair defines a line with polar and angle parameters.
 * Lines are tracked using polar (rho) and angle (theta) parameters in a space of rho/theta, in analogy with the MultiLineClusterTracker tracking
 * of rectangular objects in retinal coordinate space.
 *
 * @author tobi
 */
public class MultiLineClusterTracker extends EventFilter2D implements FrameAnnotater, Observer {
    static final double PI2=Math.PI*2;
    private static Preferences prefs=Preferences.userNodeForPackage(MultiLineClusterTracker.class);
    private java.util.List<LineCluster> clusters=new ArrayList<LineCluster>();
    
    private int eventBufferLength=prefs.getInt("MultiLineClusterTracker.eventBufferLength",30);
    {setPropertyTooltip("eventBufferLength","Number of past events to form line segments from for clustering");}
    
    private LIFOEventBuffer eventBuffer=new LIFOEventBuffer(eventBufferLength);
    LineClusterCanvas lineClusterCanvas=null;
    
    protected AEChip chip;
    private AEChipRenderer renderer;
    private int chipSize=0;
    
//    private boolean showLineClusterCanvas=false;
//    {setPropertyTooltip("showLineClusterCanvas","shows a canvas with cluster information");}
    
    private boolean showLineSegments=false;
    {setPropertyTooltip("showLineSegments","show valid LineSegments contributing to LineClusters");}
    
    private int minSegmentLength=prefs.getInt("MultiLineClusterTracker.minSegmentLength",3);
    {setPropertyTooltip("minSegmentLength","minium manhatten length of line segment in pixels");}
    private int maxSegmentLength=prefs.getInt("MultiLineClusterTracker.maxSegmentLength",20);
    {setPropertyTooltip("maxSegmentLength","max manhatten line segment length in pixels");}
    private int maxSegmentDt=prefs.getInt("MultiLineClusterTracker.maxSegmentDt",20000);
    {setPropertyTooltip("maxSegmentDt","maximum dt in ticks for line segment event pair");}
    
    /** scaling can't make cluster bigger or smaller than this ratio to default cluster size */
    public static final float MAX_SCALE_RATIO=2;
    
    
    private float rhoRadius=prefs.getFloat("MultiLineClusterTracker.rhoRadius",6);
    {setPropertyTooltip("rhoRadius","\"radius\" of line cluster around line in pixels");}
    private float thetaRadiusRad=prefs.getFloat("MultiLineClusterTracker.thetaRadiusRad",.01f);
    {setPropertyTooltip("thetaRadiusDeg","\"radius\" of line cluster in degrees around line cluster angle");}
    
    private float minDistanceNormalized=prefs.getFloat("MultiLineClusterTracker.minDistanceNormalized",0.1f);
    {setPropertyTooltip("minDistanceNormalized","minimum normalized rho and theta distance for cluster to contain segment");}
    
    private float mixingFactorRho=prefs.getFloat("MultiLineClusterTracker.mixingFactorRho",0.01f); // amount each event moves COM of cluster towards itself
    {setPropertyTooltip("mixingFactorRho","how much line cluster rho (distance from origin) is moved by a segment event");}
    private float mixingFactorTheta=prefs.getFloat("MultiLineClusterTracker.mixingFactorTheta",0.01f); // amount each event moves COM of cluster towards itself
    {setPropertyTooltip("mixingFactorTheta","how much line cluster angle is turned by a line segment event");}
    private float mixingFactorPosition=prefs.getFloat("MultiLineClusterTracker.mixingFactorPosition",0.01f);
    {setPropertyTooltip("mixingFactorPosition","how much line cluster position is moved by segment event");}
    
//    private float velocityMixingFactor=prefs.getFloat("MultiLineClusterTracker.velocityMixingFactor",0.01f); // mixing factor for velocity computation
//    {setPropertyTooltip("velocityMixingFactor","how much cluster velocity estimate is updated by each event");}
    
//    private float surround=prefs.getFloat("MultiLineClusterTracker.surround",2f);
//    {setPropertyTooltip("surround","the radius is expanded by this ratio to define events that pull radius of cluster");}
//    private boolean dynamicSizeEnabled=prefs.getBoolean("MultiLineClusterTracker.dynamicSizeEnabled", false);
//    {setPropertyTooltip("dynamicSizeEnabled","size varies dynamically depending on cluster events");}
//    private boolean dynamicAspectRatioEnabled=prefs.getBoolean("MultiLineClusterTracker.dynamicAspectRatioEnabled",false);
//    {setPropertyTooltip("dynamicAspectRatioEnabled","aspect ratio depends on events as well");}
//    private boolean pathsEnabled=prefs.getBoolean("MultiLineClusterTracker.pathsEnabled", true);
//    {setPropertyTooltip("pathsEnabled","draw paths of clusters over some window");}
    private boolean colorClustersDifferentlyEnabled=prefs.getBoolean("MultiLineClusterTracker.colorClustersDifferentlyEnabled",false);
    {setPropertyTooltip("colorClustersDifferentlyEnabled","each cluster gets assigned a random color, otherwise color indicates ages");}
    private boolean useOnePolarityOnlyEnabled=prefs.getBoolean("MultiLineClusterTracker.useOnePolarityOnlyEnabled",false);
    {setPropertyTooltip("useOnePolarityOnlyEnabled","use only one event polarity");}
    private boolean useOffPolarityOnlyEnabled=prefs.getBoolean("MultiLineClusterTracker.useOffPolarityOnlyEnabled",false);
    {setPropertyTooltip("useOffPolarityOnlyEnabled","use only OFF events, not ON - if useOnePolarityOnlyEnabled");}
    private float aspectRatio=prefs.getFloat("MultiLineClusterTracker.aspectRatio",1f);
    {setPropertyTooltip("aspectRatio","default (or starting) aspect ratio, taller is larger");}
    protected boolean growMergedSizeEnabled=prefs.getBoolean("MultiLineClusterTracker.growMergedSizeEnabled",false);
    {setPropertyTooltip("growMergedSizeEnabled","enabling makes merged clusters take on sum of sizes, otherwise they take on size of older cluster");}
    private boolean showVelocity=prefs.getBoolean("MultiLineClusterTracker.showVelocity",true); // enabling this enables both computation and rendering of cluster velocities
    {setPropertyTooltip("showVelocity","computes and shows cluster velocity");}
    private boolean logDataEnabled=false;
    {setPropertyTooltip("logDataEnabled","writes a cluster log file");}
    private PrintStream logStream=null;
    private boolean showAllClusters=prefs.getBoolean("MultiLineClusterTracker.showAllClusters",false);
    {setPropertyTooltip("showAllClusters","shows all clusters, not just those with sufficient support");}
    private boolean clusterLifetimeIncreasesWithAge=prefs.getBoolean("MultiLineClusterTracker.clusterLifetimeIncreasesWithAge",false);
    {setPropertyTooltip("clusterLifetimeIncreasesWithAge","older clusters can live longer without support, good for jumpy objects");}
    
    private final float VELOCITY_VECTOR_SCALING=1e5f; // to scale rendering of cluster velocity vector
    private int predictiveVelocityFactor=1;// making this M=10, for example, will cause cluster to substantially lead the events, then slow down, speed up, etc.
    {setPropertyTooltip("predictiveVelocityFactor","how much cluster position leads position based on estimated velocity");}
    
    private int thresholdEventsForVisibleCluster=prefs.getInt("MultiLineClusterTracker.thresholdEventsForVisibleCluster",10);
    {setPropertyTooltip("thresholdEventsForVisibleCluster","Cluster needs this many events to be visible");}
    
    private int clusterLifetimeWithoutSupportUs=prefs.getInt("MultiLineClusterTracker.clusterLifetimeWithoutSupport",10000);
    {setPropertyTooltip("clusterLifetimeWithoutSupportUs","Cluster lives this long in ticks (e.g. us) without events before pruning");}
    
    private int maxNumClusters=prefs.getInt("MultiLineClusterTracker.maxNumClusters",10);
    {setPropertyTooltip("maxNumClusters","Sets the maximum potential number of clusters");}
    
    /**
     * Creates a new instance of MultiLineClusterTracker
     * @param chip the chip we are tracking for
     */
    public MultiLineClusterTracker(AEChip chip) {
        super(chip);
        this.chip=chip;
        renderer=(AEChipRenderer)chip.getRenderer();
        chip.getRenderer().addAnnotator(this); // to draw the clusters
        chip.getCanvas().addAnnotator(this);
        initFilter();
        chip.addObserver(this); // when chip changes, we update our notion of its size, etc
//        prefs.addPreferenceChangeListener(this);
    }
    
    public void initFilter() {
        chipSize=chip.getMaxSize();
    }
    
    ArrayList<LineSegment> segList=new ArrayList<LineSegment>(); // used for rendering segments used for a packet
    
//    ArrayList<LineCluster> pruneList=new ArrayList<LineCluster>(1);
    protected LinkedList<LineCluster> pruneList=new LinkedList<LineCluster>();
    
    private LineSegment segment=new LineSegment();
    
    // the method that actually does the tracking
    synchronized private void track(EventPacket<BasicEvent> ae){
        int n=ae.getSize();
        if(n==0) return;
        int maxNumClusters=getMaxNumClusters();
        // for each event, see which cluster it is closest to and add it to this cluster.
        // if its too far from any cluster, make a new cluster if we can
//        for(int i=0;i<n;i++){
        if(showLineSegments){
            synchronized(segList) {
                segList.clear();
            }
        }
        for(BasicEvent be:ae){
            PolarityEvent event=(PolarityEvent)be;
//            System.out.println("event="+event);
            // for each past event, possibly form a line segment
            // and use it to move clusters if the segment is valid for clustering
            for(BasicEvent oe:eventBuffer){
                PolarityEvent oldEvent=(PolarityEvent)oe;
//                System.out.println("    oldEvent="+oldEvent);
                if(isValidSegment(oldEvent,event)){ // args are older,newer
                    if(showLineSegments){
                        // expensive but good for debugging
                        segment=new LineSegment(oldEvent,event);
                        segList.add(segment);
                    }else{
                        segment.set(oldEvent,event); // this reuses object, saves cost of newing segments
                    }
                    LineCluster closest=null;
                    closest=getNearestCluster(segment);
                    if( closest!=null ){
                        if(showLineSegments){
                            segment.lineCluster=closest; // we'll color this segment the color of the cluster
                        }
                        closest.addSegment(segment);
                    }else if(clusters.size()<maxNumClusters){ // start a new cluster
                        LineCluster newCluster=new LineCluster(segment);
                        clusters.add(newCluster);
                    }
                }
            }
            eventBuffer.add(event); // add the most recent event
            
        }
        
        // prune out old clusters that don't have support
        pruneList.clear();
        for(LineCluster c:clusters){
            int t0=c.getLastEventTimestamp();
            int t1=ae.getLastTimestamp();
            int timeSinceSupport=t1-t0;
            boolean killOff=false;
            if(clusterLifetimeIncreasesWithAge){
                int age=c.getLifetime();
                int supportTime=clusterLifetimeWithoutSupportUs;
                if(age<clusterLifetimeWithoutSupportUs) supportTime=age;
                if(timeSinceSupport>supportTime) killOff=true;
            }else{
                if(timeSinceSupport>clusterLifetimeWithoutSupportUs) killOff=true;
            }
            if(t0>t1 || killOff || timeSinceSupport<0){
                // ordinarily, we discard the cluster if it hasn't gotten any support for a while, but we also discard it if there
                // is something funny about the timestamps
                pruneList.add(c);
            }
//            if(t0>t1){
//                log.warning("last cluster timestamp is later than last packet timestamp");
//            }
        }
        clusters.removeAll(pruneList);
        
// merge clusters that are too close to each other.
// this must be done interatively, because merging 4 or more clusters feedforward can result in more clusters than
// you start with. each time we merge two clusters, we start over, until there are no more merges on iteration.
        
// for each cluster, if it is close to another cluster then merge them and start over.
//        pruneList.clear();
//        int beforeMergeCount=clusters.size();
        boolean mergePending;
        LineCluster c1=null,c2=null;
        do{
            mergePending=false;
            int nc=clusters.size();
            outer:
                for(int i=0;i<nc;i++){
                    c1=clusters.get(i);
                    for(int j=i+1;j<nc;j++){
                        c2=clusters.get(j); // get the other cluster
                        if(c1.isOverlapping(c2)) { // if distance is less than sum of radii merge them
                            // if cluster is close to another cluster, merge them
                            mergePending=true;
                            break outer; // break out of the outer loop
                        }
                    }
                }
                if(mergePending && c1!=null && c2!=null){
//                    pruneList.add(c1);
//                    pruneList.add(c2); // we just remove them, no need to prune them
                    clusters.remove(c1); // ok to remove because we are not iterating here
                    clusters.remove(c2);
                    clusters.add(new LineCluster(c1,c2));
                }
        }while(mergePending);
//        clusters.removeAll(pruneList);
        
// update all cluster sizes
//        // note that without this following call, clusters maintain their starting size until they are merged with another cluster.
//        if(isHighwayPerspectiveEnabled()){
//            for(LineCluster c:clusters){
//                c.setRadius(defaultClusterRadius);
//            }
//        }
        
// update paths of clusters
//        for(LineCluster c:clusters) c.updatePath();
        
//        if(clusters.size()>beforeMergeCount) throw new RuntimeException("more clusters after merge than before");
        if(isLogDataEnabled() && getNumClusters()>0){
            if(logStream!=null) {
                for(LineCluster c:clusters){
                    if(!c.isVisible()) continue;
                    logStream.println(String.format("%d %d %f %f %f", c.getClusterNumber(), c.lastTimestamp,c.location.x,c.location.y, c.averageEventDistance));
                    if(logStream.checkError()) log.warning("eroror logging data");
                }
            }
        }
        computedClusterOrdering=false;
    }
    
    public int getNumClusters(){
        return clusters.size();
    }
    
    public String toString(){
        String s="MultiLineClusterTracker with "+clusters.size()+" clusters ";
        return s;
    }
    
    /**
     * Computes signed distance to-from between two angles with cut at -PI,PI. E.g.
     *     if e is from small angle and from=PI-e, to=-PI+e, then angular distance to-from is
     *     -2e rather than (PI-e)-(-PI+e)=2PI-2e.
     *     This minimum angle difference is useful to push an angle in the correct direction
     *     by the correct amount. For this example, we want to push an angle hovering around PI-e.
     *     We don't want angles of -PI+e to push the angle from lot, just from bit towards PI. If we have from
     *     mixing factor m<<1, then new angle c=from+m*angleDistance(from,to)
     *
     *
     * @param from the first angle
     * @param to the second angle
     * @return the smallest difference to-from, ordinarily positive if to>from
     */
    private double angleDistance(double from, double to){
        double d=to-from;
        if(d>Math.PI)
            return d-Math.PI;
        if(d<-Math.PI)
            return d+Math.PI;
        return d;
    }
    
    private class LineSegment{
//        BasicEvent a=null, b=null; // we don't use these because they can get referenced out from under us
        double thetaRad=0, rhoPixels=0; // theta is angle in radians from 0 to PI with 0 and PI horizontal lines
        // rhoPixels is distance of line segment closest passage to origin of chip image LL corner
        int dx=0,dy=0, dt=0;
        int ax=0,bx=0,ay=0,by=0;
        float x=0,y=0; //TODO need float?
        int timestamp=0;
        double length;
        LineCluster lineCluster=null;
        
        /** makes an empty segment */
        LineSegment(){
        }
        
        /** Make a new LineSegment with first event a and second (later) event b */
        LineSegment(BasicEvent a, BasicEvent b){
            set(a,b);
        }
        
        /**
         computes the rho (pixels distance from origin at LL corner)
         * and theta (radian angle from 0 horizontal) for the segment given the two events.
         *This is confusing because a raw calculation can give negative rho and angle that spans -PI to PI.
         *This values are unambiguous about specifying the line segment but similar segments can have very different rho/theta values
         *depending on the quadrant that dx,dy are in. Also, the rho values -PI and PI are identical although they differ by 2*PI. This makes
         *combining values confusing.
         */
        void set(BasicEvent a, BasicEvent b){
//            this.a=a;
//            this.b=b;
            this.timestamp=b.timestamp;
            ax=a.x;
            bx=b.x;
            ay=a.y;
            by=b.y;
            x=(ax+bx)/2;
            y=(ay+by)/2;
            dt=b.timestamp-a.timestamp;
            dx=ax-bx;
            dy=ay-by; // vector (dx,dy) points from b to a (adding dx,dy to b gives a)
            length=Math.hypot(dx,dy);
            // now compute angle of dual of vector
            // dual of vector has components exchanged and one sign flipped
            // this is a rotation by 90 deg
            // then compute this angle
            // this angle is angle relative to x axis CCW of normal to dx,dy
            thetaRad=Math.atan2(dx,-dy); // atan2(y,x) goes from -PI to PI, theta goes from 0 to Pi, 0 and Pi being horizontal lines
            // now compute rho of this segment. This is closest passage to origin.
            rhoPixels=x*Math.cos(thetaRad)+y*Math.sin(thetaRad);
            // rho may come out negative, in this case we make it positive and rotate theta by PI
            if(rhoPixels<0){
                // flip rho, rotate by PI
                rhoPixels=-rhoPixels;
                if(thetaRad>0)
                    thetaRad-=Math.PI;
                else
                    thetaRad+=Math.PI;
//                thetaRad=Math.IEEEremainder(thetaRad,PI2);
            }
            //           System.out.println(String.format("x,y= %.0f,%.0f dx,dy= %d,%d, rhoPixels=%.0f thetaDeg=%.0f",x,y,dx,dy,rhoPixels, Math.toDegrees(thetaRad)));
        }
        
//        /** @return normalized "distance" between two segments based on polar representation. Are segments co-linear? If so measure should
//         be small.
//         */
//        double distance2To(LineSegment b){
//            double dtheta=(thetaRad-b.thetaRad)/Math.PI/2; // problem is that 0 and PI are close
//            dtheta*=dtheta;
//            double drho=(rhoPixels-b.rhoPixels)/chip.getMaxSize();
//            drho*=drho;
//            return (dtheta+drho);
//        }
        
        public String toString(){
            return String.format("LineSegment a x,y=%d,%d, b x,y=%d,%d, rho=%.1f pix theta=%.0f deg x,y=%.1f,%.1f", ax,ay,bx,by,rhoPixels, Math.toDegrees(thetaRad),x,y);
        }
        
        private void draw(GL gl) {
            if(lineCluster!=null){
                gl.glColor3fv(lineCluster.rgb,0);
//                System.out.println(this);
            }else{
                gl.glColor3f(.2f,.2f,.2f);
            }
            gl.glPushMatrix();
            gl.glLineWidth(1);
            
            gl.glBegin(GL.GL_LINE_LOOP);
            gl.glVertex2i(ax,ay);
            gl.glVertex2i(bx,by);
            gl.glEnd();
            gl.glPopMatrix();
        }
        
        
    }
    /** Checks two events to see if they could form a valid line segment.
     The events need to have the same Polarity is the event is a PolarityEvent.
     @return segment if line segment is long enough but not too long and is not based on events too far apart in time. Returns null if not
     a goog segment*/
    private boolean isValidSegment(BasicEvent older, BasicEvent newer){
        if(older instanceof PolarityEvent){ // assume same type
            if(((PolarityEvent)older).polarity != ((PolarityEvent)newer).polarity) return false;
        };
        int dx=Math.abs(newer.x-older.x), dy=Math.abs(older.y-newer.y);
        if(dx<minSegmentLength && dy<minSegmentLength) return false;
        if(dx>maxSegmentLength || dy>maxSegmentLength) return false;
        int dt=newer.timestamp-older.timestamp;
        if(dt<0){
//            log.warning("older event is later than newer event; negative dt="+dt+" older="+older+" newer="+newer);
            return false;
        }
         if(dt>maxSegmentDt) return false;
       
        return true;
    }
    
    /**
     * Method that given segment event, returns closest cluster and distance to it. The actual computation returns the first cluster that is within the
     * minDistance of the event, which reduces the computation at the cost of reduced precision.
     * @param segment the segment event
     * @return closest cluster object (a cluster with a distance - that distance is the distance between the given event and the returned cluster).
     */
    private LineCluster getNearestCluster(LineSegment segment){
        boolean useFirst=false;
        
        double minDistance=Double.MAX_VALUE;
        LineCluster closest=null;
        double currentDistance;
        for(LineCluster c:clusters){
            if(!useFirst){
                double d=c.distanceAbsTo(segment);
                if(d<minDistance ){
                    minDistance=d;
                    closest=c;
                }
            }else{
                if(c.contains(segment)){
                    closest=c;
                    return closest;
//                System.out.println("contains: dRho="+(c.rhoPixels-segment.rhoPixels)+" dTheta="+(c.thetaRad-segment.thetaRad));
                }
            }
        }
        if(!useFirst){
            if(minDistance<minDistanceNormalized){
                return closest;
            }else{
                return null;
            }
        }else{
            return closest;
        }
    }
    
    protected int clusterCounter=0; // keeps track of absolute cluster number
    
    /** Represents a single tracked object */
    public class LineCluster{
        
        /** center or average line location of cluster in pixels */
        public Point2D.Float location=new Point2D.Float(); // location in chip pixels
        
        /** Distance of line from passing origin in pixels */
        public double rhoPixels=0;
        
        /** Angle of line CCW from x axis with 0 being horizontal and Pi/2 being vertical */
        public double thetaRad=0;
        
        /** velocity of cluster in pixels/tick, where tick is timestamp tick (usually microseconds) */
        public Point2D.Float velocity=new Point2D.Float(); // velocity in chip pixels/sec
        
        protected final int MAX_PATH_LENGTH=100;
//        protected ArrayList<Point2D.Float> path=new ArrayList<Point2D.Float>(MAX_PATH_LENGTH);
        protected Color color=null;
        float[] rgb=new float[4];
        
        protected int numEvents;
//        ArrayList<EventXYType> events=new ArrayList<EventXYType>();
        protected int lastTimestamp, firstTimestamp;
        protected float instantaneousEventRate; // in events/tick
        private float avgEventRate = 0;
        protected float instantaneousISI; // ticks/event
        private float avgISI;
        private int clusterNumber; // assigned to be the absolute number of the cluster that has been created
        private double averageEventDistance; // average (mixed) distance of events from cluster center, a measure of actual cluster size
        public double distanceToLastEvent=Float.POSITIVE_INFINITY;
        
        public double length=0;
        
        
        
        public LineCluster(){
            float hue=random.nextFloat();
            Color color=Color.getHSBColor(hue,1f,1f);
            setColor(color);
            setClusterNumber(clusterCounter++);
        }
        
        public LineCluster(LineSegment s){
            this();
            rhoPixels=s.rhoPixels;
            thetaRad=s.thetaRad;
            location.x=s.x;
            location.y=s.y;
            lastTimestamp=s.timestamp;
            firstTimestamp=lastTimestamp;
            numEvents=1;
            s.lineCluster=this;
        }
        
//        /**
//         * Computes a geometrical scale factor based on location of a point relative to the vanishing point.
//         * If a pixel has been selected (we ask the renderer) then we compute the perspective from this vanishing point, otherwise
//         * it is the top middle pixel.
//         * @param p a point with 0,0 at lower left corner
//         * @return scale factor, which grows linearly to 1 at botton of scene
//         */
//        final float getPerspectiveScaleFactor(Point2D.Float p){
//            if(!renderer.isPixelSelected()){
//                float yfrac=1f-(p.y/chip.getSizeY()); // yfrac grows to 1 at bottom of image
//                return yfrac;
//            }else{
//                // scale is 0 at vanishing point and grows linearly to 1 at max size of chip
//                int size=chip.getMaxSize();
//                float d=(float)p.distance(renderer.getXsel(),renderer.getYsel());
//                float scale=d/size;
//                return scale;
//            }
//        }
//
        /** Constructs a cluster by merging two clusters. All parameters of the resulting cluster should be reasonable combinations of the
         * source cluster parameters. For example, the merged location values are weighted by the number of events that have supported each
         * source cluster, so that older clusters weigh more heavily in the resulting cluster location. Subtle bugs or poor performance can result
         * from not properly handling the merging of parameters.
         *
         * @param one the first cluster
         * @param two the second cluster
         */
        public LineCluster(LineCluster one, LineCluster two){
            this();
            // merge locations by just averaging
//            location.x=(one.location.x+two.location.x)/2;
//            location.y=(one.location.y+two.location.y)/2;
            
            LineCluster older=one.firstTimestamp<two.firstTimestamp? one:two;
//            LineCluster older=one.numEvents>two.numEvents? one:two;
            
            // merge locations by average weighted by number of events supporting cluster
            int sumEvents=one.numEvents+two.numEvents;
            location.x=(one.location.x*one.numEvents+two.location.x*two.numEvents)/(sumEvents);
            location.y=(one.location.y*one.numEvents+two.location.y*two.numEvents)/(sumEvents);
            averageEventDistance=( one.averageEventDistance*one.numEvents + two.averageEventDistance*two.numEvents )/sumEvents;
            lastTimestamp=(one.lastTimestamp+two.lastTimestamp)/2;
            numEvents=sumEvents;
            firstTimestamp=older.firstTimestamp; // make lifetime the oldest src cluster
            lastTimestamp=older.lastTimestamp;
//            path=older.path;
            velocity.x=older.velocity.x;
            velocity.y=older.velocity.y;
            avgEventRate=older.avgEventRate;
            avgISI=older.avgISI;
            
            rhoPixels=older.rhoPixels;
            thetaRad=older.thetaRad;
            
            length=older.length;
            
            setColor(older.getColor());
            
        }
        
        public int getLastEventTimestamp(){<commit_msg>not copying velocity fields because not used.<commit_after>/*
 * MultiLineClusterTracker.java
 *
 * Created on December 5, 2005, 3:49 AM
 *
 * To change this template, choose Tools | Options and locate the template under
 * the Source Creation and Management node. Right-click the template and choose
 * Open. You can then make changes to the template in the Source Editor.
 */

package ch.unizh.ini.caviar.eventprocessing.tracking;
import ch.unizh.ini.caviar.aemonitor.AEConstants;
import ch.unizh.ini.caviar.chip.*;
import ch.unizh.ini.caviar.eventprocessing.EventFilter2D;
import ch.unizh.ini.caviar.event.*;
import ch.unizh.ini.caviar.event.EventPacket;
import ch.unizh.ini.caviar.eventprocessing.TimeLimiter;
import ch.unizh.ini.caviar.graphics.*;
import com.sun.opengl.util.*;
import java.awt.*;
//import ch.unizh.ini.caviar.util.PreferencesEditor;
import java.awt.geom.*;
import java.io.*;
import java.util.*;
import java.util.prefs.*;
import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCanvas;
import javax.media.opengl.GLCapabilities;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.glu.GLU;
import javax.swing.*;

/**
 * Tracks multiple lines in the scene using a cluster based method based on pairs of recent events.
 * The event pairs come from a buffer formed from recent events. Each pair defines a line with polar and angle parameters.
 * Lines are tracked using polar (rho) and angle (theta) parameters in a space of rho/theta, in analogy with the MultiLineClusterTracker tracking
 * of rectangular objects in retinal coordinate space.
 *
 * @author tobi
 */
public class MultiLineClusterTracker extends EventFilter2D implements FrameAnnotater, Observer {
    static final double PI2=Math.PI*2;
    private static Preferences prefs=Preferences.userNodeForPackage(MultiLineClusterTracker.class);
    private java.util.List<LineCluster> clusters=new ArrayList<LineCluster>();
    
    private int eventBufferLength=prefs.getInt("MultiLineClusterTracker.eventBufferLength",30);
    {setPropertyTooltip("eventBufferLength","Number of past events to form line segments from for clustering");}
    
    private LIFOEventBuffer eventBuffer=new LIFOEventBuffer(eventBufferLength);
    LineClusterCanvas lineClusterCanvas=null;
    
    protected AEChip chip;
    private AEChipRenderer renderer;
    private int chipSize=0;
    
//    private boolean showLineClusterCanvas=false;
//    {setPropertyTooltip("showLineClusterCanvas","shows a canvas with cluster information");}
    
    private boolean showLineSegments=false;
    {setPropertyTooltip("showLineSegments","show valid LineSegments contributing to LineClusters");}
    
    private int minSegmentLength=prefs.getInt("MultiLineClusterTracker.minSegmentLength",3);
    {setPropertyTooltip("minSegmentLength","minium manhatten length of line segment in pixels");}
    private int maxSegmentLength=prefs.getInt("MultiLineClusterTracker.maxSegmentLength",20);
    {setPropertyTooltip("maxSegmentLength","max manhatten line segment length in pixels");}
    private int maxSegmentDt=prefs.getInt("MultiLineClusterTracker.maxSegmentDt",20000);
    {setPropertyTooltip("maxSegmentDt","maximum dt in ticks for line segment event pair");}
    
    /** scaling can't make cluster bigger or smaller than this ratio to default cluster size */
    public static final float MAX_SCALE_RATIO=2;
    
    
    private float rhoRadius=prefs.getFloat("MultiLineClusterTracker.rhoRadius",6);
    {setPropertyTooltip("rhoRadius","\"radius\" of line cluster around line in pixels");}
    private float thetaRadiusRad=prefs.getFloat("MultiLineClusterTracker.thetaRadiusRad",.01f);
    {setPropertyTooltip("thetaRadiusDeg","\"radius\" of line cluster in degrees around line cluster angle");}
    
    private float minDistanceNormalized=prefs.getFloat("MultiLineClusterTracker.minDistanceNormalized",0.1f);
    {setPropertyTooltip("minDistanceNormalized","minimum normalized rho and theta distance for cluster to contain segment");}
    
    private float mixingFactorRho=prefs.getFloat("MultiLineClusterTracker.mixingFactorRho",0.01f); // amount each event moves COM of cluster towards itself
    {setPropertyTooltip("mixingFactorRho","how much line cluster rho (distance from origin) is moved by a segment event");}
    private float mixingFactorTheta=prefs.getFloat("MultiLineClusterTracker.mixingFactorTheta",0.01f); // amount each event moves COM of cluster towards itself
    {setPropertyTooltip("mixingFactorTheta","how much line cluster angle is turned by a line segment event");}
    private float mixingFactorPosition=prefs.getFloat("MultiLineClusterTracker.mixingFactorPosition",0.01f);
    {setPropertyTooltip("mixingFactorPosition","how much line cluster position is moved by segment event");}
    
//    private float velocityMixingFactor=prefs.getFloat("MultiLineClusterTracker.velocityMixingFactor",0.01f); // mixing factor for velocity computation
//    {setPropertyTooltip("velocityMixingFactor","how much cluster velocity estimate is updated by each event");}
    
//    private float surround=prefs.getFloat("MultiLineClusterTracker.surround",2f);
//    {setPropertyTooltip("surround","the radius is expanded by this ratio to define events that pull radius of cluster");}
//    private boolean dynamicSizeEnabled=prefs.getBoolean("MultiLineClusterTracker.dynamicSizeEnabled", false);
//    {setPropertyTooltip("dynamicSizeEnabled","size varies dynamically depending on cluster events");}
//    private boolean dynamicAspectRatioEnabled=prefs.getBoolean("MultiLineClusterTracker.dynamicAspectRatioEnabled",false);
//    {setPropertyTooltip("dynamicAspectRatioEnabled","aspect ratio depends on events as well");}
//    private boolean pathsEnabled=prefs.getBoolean("MultiLineClusterTracker.pathsEnabled", true);
//    {setPropertyTooltip("pathsEnabled","draw paths of clusters over some window");}
    private boolean colorClustersDifferentlyEnabled=prefs.getBoolean("MultiLineClusterTracker.colorClustersDifferentlyEnabled",false);
    {setPropertyTooltip("colorClustersDifferentlyEnabled","each cluster gets assigned a random color, otherwise color indicates ages");}
    private boolean useOnePolarityOnlyEnabled=prefs.getBoolean("MultiLineClusterTracker.useOnePolarityOnlyEnabled",false);
    {setPropertyTooltip("useOnePolarityOnlyEnabled","use only one event polarity");}
    private boolean useOffPolarityOnlyEnabled=prefs.getBoolean("MultiLineClusterTracker.useOffPolarityOnlyEnabled",false);
    {setPropertyTooltip("useOffPolarityOnlyEnabled","use only OFF events, not ON - if useOnePolarityOnlyEnabled");}
    private float aspectRatio=prefs.getFloat("MultiLineClusterTracker.aspectRatio",1f);
    {setPropertyTooltip("aspectRatio","default (or starting) aspect ratio, taller is larger");}
    protected boolean growMergedSizeEnabled=prefs.getBoolean("MultiLineClusterTracker.growMergedSizeEnabled",false);
    {setPropertyTooltip("growMergedSizeEnabled","enabling makes merged clusters take on sum of sizes, otherwise they take on size of older cluster");}
    private boolean showVelocity=prefs.getBoolean("MultiLineClusterTracker.showVelocity",true); // enabling this enables both computation and rendering of cluster velocities
    {setPropertyTooltip("showVelocity","computes and shows cluster velocity");}
    private boolean logDataEnabled=false;
    {setPropertyTooltip("logDataEnabled","writes a cluster log file");}
    private PrintStream logStream=null;
    private boolean showAllClusters=prefs.getBoolean("MultiLineClusterTracker.showAllClusters",false);
    {setPropertyTooltip("showAllClusters","shows all clusters, not just those with sufficient support");}
    private boolean clusterLifetimeIncreasesWithAge=prefs.getBoolean("MultiLineClusterTracker.clusterLifetimeIncreasesWithAge",false);
    {setPropertyTooltip("clusterLifetimeIncreasesWithAge","older clusters can live longer without support, good for jumpy objects");}
    
    private final float VELOCITY_VECTOR_SCALING=1e5f; // to scale rendering of cluster velocity vector
    private int predictiveVelocityFactor=1;// making this M=10, for example, will cause cluster to substantially lead the events, then slow down, speed up, etc.
    {setPropertyTooltip("predictiveVelocityFactor","how much cluster position leads position based on estimated velocity");}
    
    private int thresholdEventsForVisibleCluster=prefs.getInt("MultiLineClusterTracker.thresholdEventsForVisibleCluster",10);
    {setPropertyTooltip("thresholdEventsForVisibleCluster","Cluster needs this many events to be visible");}
    
    private int clusterLifetimeWithoutSupportUs=prefs.getInt("MultiLineClusterTracker.clusterLifetimeWithoutSupport",10000);
    {setPropertyTooltip("clusterLifetimeWithoutSupportUs","Cluster lives this long in ticks (e.g. us) without events before pruning");}
    
    private int maxNumClusters=prefs.getInt("MultiLineClusterTracker.maxNumClusters",10);
    {setPropertyTooltip("maxNumClusters","Sets the maximum potential number of clusters");}
    
    /**
     * Creates a new instance of MultiLineClusterTracker
     * @param chip the chip we are tracking for
     */
    public MultiLineClusterTracker(AEChip chip) {
        super(chip);
        this.chip=chip;
        renderer=(AEChipRenderer)chip.getRenderer();
        chip.getRenderer().addAnnotator(this); // to draw the clusters
        chip.getCanvas().addAnnotator(this);
        initFilter();
        chip.addObserver(this); // when chip changes, we update our notion of its size, etc
//        prefs.addPreferenceChangeListener(this);
    }
    
    public void initFilter() {
        chipSize=chip.getMaxSize();
    }
    
    ArrayList<LineSegment> segList=new ArrayList<LineSegment>(); // used for rendering segments used for a packet
    
//    ArrayList<LineCluster> pruneList=new ArrayList<LineCluster>(1);
    protected LinkedList<LineCluster> pruneList=new LinkedList<LineCluster>();
    
    private LineSegment segment=new LineSegment();
    
    // the method that actually does the tracking
    synchronized private void track(EventPacket<BasicEvent> ae){
        int n=ae.getSize();
        if(n==0) return;
        int maxNumClusters=getMaxNumClusters();
        // for each event, see which cluster it is closest to and add it to this cluster.
        // if its too far from any cluster, make a new cluster if we can
//        for(int i=0;i<n;i++){
        if(showLineSegments){
            synchronized(segList) {
                segList.clear();
            }
        }
        for(BasicEvent be:ae){
            PolarityEvent event=(PolarityEvent)be;
//            System.out.println("event="+event);
            // for each past event, possibly form a line segment
            // and use it to move clusters if the segment is valid for clustering
            for(BasicEvent oe:eventBuffer){
                PolarityEvent oldEvent=(PolarityEvent)oe;
//                System.out.println("    oldEvent="+oldEvent);
                if(isValidSegment(oldEvent,event)){ // args are older,newer
                    if(showLineSegments){
                        // expensive but good for debugging
                        segment=new LineSegment(oldEvent,event);
                        segList.add(segment);
                    }else{
                        segment.set(oldEvent,event); // this reuses object, saves cost of newing segments
                    }
                    LineCluster closest=null;
                    closest=getNearestCluster(segment);
                    if( closest!=null ){
                        if(showLineSegments){
                            segment.lineCluster=closest; // we'll color this segment the color of the cluster
                        }
                        closest.addSegment(segment);
                    }else if(clusters.size()<maxNumClusters){ // start a new cluster
                        LineCluster newCluster=new LineCluster(segment);
                        clusters.add(newCluster);
                    }
                }
            }
            eventBuffer.add(event); // add the most recent event
            
        }
        
        // prune out old clusters that don't have support
        pruneList.clear();
        for(LineCluster c:clusters){
            int t0=c.getLastEventTimestamp();
            int t1=ae.getLastTimestamp();
            int timeSinceSupport=t1-t0;
            boolean killOff=false;
            if(clusterLifetimeIncreasesWithAge){
                int age=c.getLifetime();
                int supportTime=clusterLifetimeWithoutSupportUs;
                if(age<clusterLifetimeWithoutSupportUs) supportTime=age;
                if(timeSinceSupport>supportTime) killOff=true;
            }else{
                if(timeSinceSupport>clusterLifetimeWithoutSupportUs) killOff=true;
            }
            if(t0>t1 || killOff || timeSinceSupport<0){
                // ordinarily, we discard the cluster if it hasn't gotten any support for a while, but we also discard it if there
                // is something funny about the timestamps
                pruneList.add(c);
            }
//            if(t0>t1){
//                log.warning("last cluster timestamp is later than last packet timestamp");
//            }
        }
        clusters.removeAll(pruneList);
        
// merge clusters that are too close to each other.
// this must be done interatively, because merging 4 or more clusters feedforward can result in more clusters than
// you start with. each time we merge two clusters, we start over, until there are no more merges on iteration.
        
// for each cluster, if it is close to another cluster then merge them and start over.
//        pruneList.clear();
//        int beforeMergeCount=clusters.size();
        boolean mergePending;
        LineCluster c1=null,c2=null;
        do{
            mergePending=false;
            int nc=clusters.size();
            outer:
                for(int i=0;i<nc;i++){
                    c1=clusters.get(i);
                    for(int j=i+1;j<nc;j++){
                        c2=clusters.get(j); // get the other cluster
                        if(c1.isOverlapping(c2)) { // if distance is less than sum of radii merge them
                            // if cluster is close to another cluster, merge them
                            mergePending=true;
                            break outer; // break out of the outer loop
                        }
                    }
                }
                if(mergePending && c1!=null && c2!=null){
//                    pruneList.add(c1);
//                    pruneList.add(c2); // we just remove them, no need to prune them
                    clusters.remove(c1); // ok to remove because we are not iterating here
                    clusters.remove(c2);
                    clusters.add(new LineCluster(c1,c2));
                }
        }while(mergePending);
//        clusters.removeAll(pruneList);
        
// update all cluster sizes
//        // note that without this following call, clusters maintain their starting size until they are merged with another cluster.
//        if(isHighwayPerspectiveEnabled()){
//            for(LineCluster c:clusters){
//                c.setRadius(defaultClusterRadius);
//            }
//        }
        
// update paths of clusters
//        for(LineCluster c:clusters) c.updatePath();
        
//        if(clusters.size()>beforeMergeCount) throw new RuntimeException("more clusters after merge than before");
        if(isLogDataEnabled() && getNumClusters()>0){
            if(logStream!=null) {
                for(LineCluster c:clusters){
                    if(!c.isVisible()) continue;
                    logStream.println(String.format("%d %d %f %f %f", c.getClusterNumber(), c.lastTimestamp,c.location.x,c.location.y, c.averageEventDistance));
                    if(logStream.checkError()) log.warning("eroror logging data");
                }
            }
        }
        computedClusterOrdering=false;
    }
    
    public int getNumClusters(){
        return clusters.size();
    }
    
    public String toString(){
        String s="MultiLineClusterTracker with "+clusters.size()+" clusters ";
        return s;
    }
    
    /**
     * Computes signed distance to-from between two angles with cut at -PI,PI. E.g.
     *     if e is from small angle and from=PI-e, to=-PI+e, then angular distance to-from is
     *     -2e rather than (PI-e)-(-PI+e)=2PI-2e.
     *     This minimum angle difference is useful to push an angle in the correct direction
     *     by the correct amount. For this example, we want to push an angle hovering around PI-e.
     *     We don't want angles of -PI+e to push the angle from lot, just from bit towards PI. If we have from
     *     mixing factor m<<1, then new angle c=from+m*angleDistance(from,to)
     *
     *
     * @param from the first angle
     * @param to the second angle
     * @return the smallest difference to-from, ordinarily positive if to>from
     */
    private double angleDistance(double from, double to){
        double d=to-from;
        if(d>Math.PI)
            return d-Math.PI;
        if(d<-Math.PI)
            return d+Math.PI;
        return d;
    }
    
    private class LineSegment{
//        BasicEvent a=null, b=null; // we don't use these because they can get referenced out from under us
        double thetaRad=0, rhoPixels=0; // theta is angle in radians from 0 to PI with 0 and PI horizontal lines
        // rhoPixels is distance of line segment closest passage to origin of chip image LL corner
        int dx=0,dy=0, dt=0;
        int ax=0,bx=0,ay=0,by=0;
        float x=0,y=0; //TODO need float?
        int timestamp=0;
        double length;
        LineCluster lineCluster=null;
        
        /** makes an empty segment */
        LineSegment(){
        }
        
        /** Make a new LineSegment with first event a and second (later) event b */
        LineSegment(BasicEvent a, BasicEvent b){
            set(a,b);
        }
        
        /**
         computes the rho (pixels distance from origin at LL corner)
         * and theta (radian angle from 0 horizontal) for the segment given the two events.
         *This is confusing because a raw calculation can give negative rho and angle that spans -PI to PI.
         *This values are unambiguous about specifying the line segment but similar segments can have very different rho/theta values
         *depending on the quadrant that dx,dy are in. Also, the rho values -PI and PI are identical although they differ by 2*PI. This makes
         *combining values confusing.
         */
        void set(BasicEvent a, BasicEvent b){
//            this.a=a;
//            this.b=b;
            this.timestamp=b.timestamp;
            ax=a.x;
            bx=b.x;
            ay=a.y;
            by=b.y;
            x=(ax+bx)/2;
            y=(ay+by)/2;
            dt=b.timestamp-a.timestamp;
            dx=ax-bx;
            dy=ay-by; // vector (dx,dy) points from b to a (adding dx,dy to b gives a)
            length=Math.hypot(dx,dy);
            // now compute angle of dual of vector
            // dual of vector has components exchanged and one sign flipped
            // this is a rotation by 90 deg
            // then compute this angle
            // this angle is angle relative to x axis CCW of normal to dx,dy
            thetaRad=Math.atan2(dx,-dy); // atan2(y,x) goes from -PI to PI, theta goes from 0 to Pi, 0 and Pi being horizontal lines
            // now compute rho of this segment. This is closest passage to origin.
            rhoPixels=x*Math.cos(thetaRad)+y*Math.sin(thetaRad);
            // rho may come out negative, in this case we make it positive and rotate theta by PI
            if(rhoPixels<0){
                // flip rho, rotate by PI
                rhoPixels=-rhoPixels;
                if(thetaRad>0)
                    thetaRad-=Math.PI;
                else
                    thetaRad+=Math.PI;
//                thetaRad=Math.IEEEremainder(thetaRad,PI2);
            }
            //           System.out.println(String.format("x,y= %.0f,%.0f dx,dy= %d,%d, rhoPixels=%.0f thetaDeg=%.0f",x,y,dx,dy,rhoPixels, Math.toDegrees(thetaRad)));
        }
        
//        /** @return normalized "distance" between two segments based on polar representation. Are segments co-linear? If so measure should
//         be small.
//         */
//        double distance2To(LineSegment b){
//            double dtheta=(thetaRad-b.thetaRad)/Math.PI/2; // problem is that 0 and PI are close
//            dtheta*=dtheta;
//            double drho=(rhoPixels-b.rhoPixels)/chip.getMaxSize();
//            drho*=drho;
//            return (dtheta+drho);
//        }
        
        public String toString(){
            return String.format("LineSegment a x,y=%d,%d, b x,y=%d,%d, rho=%.1f pix theta=%.0f deg x,y=%.1f,%.1f", ax,ay,bx,by,rhoPixels, Math.toDegrees(thetaRad),x,y);
        }
        
        private void draw(GL gl) {
            if(lineCluster!=null){
                gl.glColor3fv(lineCluster.rgb,0);
//                System.out.println(this);
            }else{
                gl.glColor3f(.2f,.2f,.2f);
            }
            gl.glPushMatrix();
            gl.glLineWidth(1);
            
            gl.glBegin(GL.GL_LINE_LOOP);
            gl.glVertex2i(ax,ay);
            gl.glVertex2i(bx,by);
            gl.glEnd();
            gl.glPopMatrix();
        }
        
        
    }
    /** Checks two events to see if they could form a valid line segment.
     The events need to have the same Polarity is the event is a PolarityEvent.
     @return segment if line segment is long enough but not too long and is not based on events too far apart in time. Returns null if not
     a goog segment*/
    private boolean isValidSegment(BasicEvent older, BasicEvent newer){
        if(older instanceof PolarityEvent){ // assume same type
            if(((PolarityEvent)older).polarity != ((PolarityEvent)newer).polarity) return false;
        };
        int dx=Math.abs(newer.x-older.x), dy=Math.abs(older.y-newer.y);
        if(dx<minSegmentLength && dy<minSegmentLength) return false;
        if(dx>maxSegmentLength || dy>maxSegmentLength) return false;
        int dt=newer.timestamp-older.timestamp;
        if(dt<0){
//            log.warning("older event is later than newer event; negative dt="+dt+" older="+older+" newer="+newer);
            return false;
        }
         if(dt>maxSegmentDt) return false;
       
        return true;
    }
    
    /**
     * Method that given segment event, returns closest cluster and distance to it. The actual computation returns the first cluster that is within the
     * minDistance of the event, which reduces the computation at the cost of reduced precision.
     * @param segment the segment event
     * @return closest cluster object (a cluster with a distance - that distance is the distance between the given event and the returned cluster).
     */
    private LineCluster getNearestCluster(LineSegment segment){
        boolean useFirst=false;
        
        double minDistance=Double.MAX_VALUE;
        LineCluster closest=null;
        double currentDistance;
        for(LineCluster c:clusters){
            if(!useFirst){
                double d=c.distanceAbsTo(segment);
                if(d<minDistance ){
                    minDistance=d;
                    closest=c;
                }
            }else{
                if(c.contains(segment)){
                    closest=c;
                    return closest;
//                System.out.println("contains: dRho="+(c.rhoPixels-segment.rhoPixels)+" dTheta="+(c.thetaRad-segment.thetaRad));
                }
            }
        }
        if(!useFirst){
            if(minDistance<minDistanceNormalized){
                return closest;
            }else{
                return null;
            }
        }else{
            return closest;
        }
    }
    
    protected int clusterCounter=0; // keeps track of absolute cluster number
    
    /** Represents a single tracked object */
    public class LineCluster{
        
        /** center or average line location of cluster in pixels */
        public Point2D.Float location=new Point2D.Float(); // location in chip pixels
        
        /** Distance of line from passing origin in pixels */
        public double rhoPixels=0;
        
        /** Angle of line CCW from x axis with 0 being horizontal and Pi/2 being vertical */
        public double thetaRad=0;
        
        /** velocity of cluster in pixels/tick, where tick is timestamp tick (usually microseconds) */
        public Point2D.Float velocity=new Point2D.Float(); // velocity in chip pixels/sec
        
        protected final int MAX_PATH_LENGTH=100;
//        protected ArrayList<Point2D.Float> path=new ArrayList<Point2D.Float>(MAX_PATH_LENGTH);
        protected Color color=null;
        float[] rgb=new float[4];
        
        protected int numEvents;
//        ArrayList<EventXYType> events=new ArrayList<EventXYType>();
        protected int lastTimestamp, firstTimestamp;
        protected float instantaneousEventRate; // in events/tick
        private float avgEventRate = 0;
        protected float instantaneousISI; // ticks/event
        private float avgISI;
        private int clusterNumber; // assigned to be the absolute number of the cluster that has been created
        private double averageEventDistance; // average (mixed) distance of events from cluster center, a measure of actual cluster size
        public double distanceToLastEvent=Float.POSITIVE_INFINITY;
        
        public double length=0;
        
        
        
        public LineCluster(){
            float hue=random.nextFloat();
            Color color=Color.getHSBColor(hue,1f,1f);
            setColor(color);
            setClusterNumber(clusterCounter++);
        }
        
        public LineCluster(LineSegment s){
            this();
            rhoPixels=s.rhoPixels;
            thetaRad=s.thetaRad;
            location.x=s.x;
            location.y=s.y;
            lastTimestamp=s.timestamp;
            firstTimestamp=lastTimestamp;
            numEvents=1;
            s.lineCluster=this;
        }
        
//        /**
//         * Computes a geometrical scale factor based on location of a point relative to the vanishing point.
//         * If a pixel has been selected (we ask the renderer) then we compute the perspective from this vanishing point, otherwise
//         * it is the top middle pixel.
//         * @param p a point with 0,0 at lower left corner
//         * @return scale factor, which grows linearly to 1 at botton of scene
//         */
//        final float getPerspectiveScaleFactor(Point2D.Float p){
//            if(!renderer.isPixelSelected()){
//                float yfrac=1f-(p.y/chip.getSizeY()); // yfrac grows to 1 at bottom of image
//                return yfrac;
//            }else{
//                // scale is 0 at vanishing point and grows linearly to 1 at max size of chip
//                int size=chip.getMaxSize();
//                float d=(float)p.distance(renderer.getXsel(),renderer.getYsel());
//                float scale=d/size;
//                return scale;
//            }
//        }
//
        /** Constructs a cluster by merging two clusters. All parameters of the resulting cluster should be reasonable combinations of the
         * source cluster parameters. For example, the merged location values are weighted by the number of events that have supported each
         * source cluster, so that older clusters weigh more heavily in the resulting cluster location. Subtle bugs or poor performance can result
         * from not properly handling the merging of parameters.
         *
         * @param one the first cluster
         * @param two the second cluster
         */
        public LineCluster(LineCluster one, LineCluster two){
            this();
            // merge locations by just averaging
//            location.x=(one.location.x+two.location.x)/2;
//            location.y=(one.location.y+two.location.y)/2;
            
            LineCluster older=one.firstTimestamp<two.firstTimestamp? one:two;
//            LineCluster older=one.numEvents>two.numEvents? one:two;
            
            // merge locations by average weighted by number of events supporting cluster
            int sumEvents=one.numEvents+two.numEvents;
            location.x=(one.location.x*one.numEvents+two.location.x*two.numEvents)/(sumEvents);
            location.y=(one.location.y*one.numEvents+two.location.y*two.numEvents)/(sumEvents);
            averageEventDistance=( one.averageEventDistance*one.numEvents + two.averageEventDistance*two.numEvents )/sumEvents;
            lastTimestamp=(one.lastTimestamp+two.lastTimestamp)/2;
            numEvents=sumEvents;
            firstTimestamp=older.firstTimestamp; // make lifetime the oldest src cluster
            lastTimestamp=older.lastTimestamp;
//            path=older.path;
//            velocity.x=older.velocity.x;
//            velocity.y=older.velocity.y;
            avgEventRate=older.avgEventRate;
            avgISI=older.avgISI;
            
            rhoPixels=older.rhoPixels;
            thetaRad=older.thetaRad;
            
            length=older.length;
            
            setColor(older.getColor());
            
        }
        
        public int getLastEventTimestamp(){
{'fraction_non_alphanumeric': 0.09199810766059541, 'fraction_numerical': 0.005744601763930659, 'mean_word_length': 2.4283480074142725, 'pattern_counts': {'":': 0, '<': 61, '<?xml version=': 0, '>': 55, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6543926', 'n_tokens_mistral': 15686, 'n_tokens_neox': 15326, 'n_words': 4581}