public Boundary(Boundary b)
 {            
     this.Minx = b.Minx;
     this.Miny = b.Miny;
     this.Maxx = b.Maxx;
     this.Maxy = b.Maxy;
 }
        public GridCluster(IPoints dataset, JsonGetMarkersReceive jsonReceive)
            : base(dataset)
        {
            // Important, set _delta and _grid values in constructor as first step
            var deltas = GetDelta(jsonReceive);
            DeltaX = deltas[0];
            DeltaY = deltas[1];
            Grid = GetBoundaryExtended(jsonReceive);
            Lines = new List<Line>();

            if (AlgoConfig.DoShowGridLinesInGoogleMap) MakeLines(jsonReceive);
        }
        static IPoints GenerateRandomDataset(int n, Boundary b)
        {
            b.Normalize();

            // Random points
            var list = new List<IP>();
            for (var i = 0; i < n; i++)
            {
                var lat = (b.Miny + b.AbsY * Rand.NextDouble()).NormalizeLatitude();
                var lon = (b.Minx + b.AbsX * Rand.NextDouble()).NormalizeLongitude();
                list.Add(new P { I = Rand.Next(1000000000), C = 1, Y = lat, X = lon, T = i });
            }

            return new Points {Data = list};
        }
        public static Boundary GetBoundaryExtended(JsonGetMarkersReceive jsonReceive)
        {
            var deltas = GetDelta(jsonReceive);
            var deltaX = deltas[0];
            var deltaY = deltas[1];

            // Grid with extended outer grid-area non-visible            
            var a = MathTool.FloorLatLon(jsonReceive.Viewport.Minx, deltaX) - deltaX * AlgoConfig.OuterGridExtend;
            var b = MathTool.FloorLatLon(jsonReceive.Viewport.Miny, deltaY) - deltaY * AlgoConfig.OuterGridExtend;
            var a2 = MathTool.FloorLatLon(jsonReceive.Viewport.Maxx, deltaX) + deltaX * (1 + AlgoConfig.OuterGridExtend);
            var b2 = MathTool.FloorLatLon(jsonReceive.Viewport.Maxy, deltaY) + deltaY * (1 + AlgoConfig.OuterGridExtend);

            // Latitude is special with Google Maps, they don't wrap around, then do constrain
            b = MathTool.ConstrainLatitude(b);
            b2 = MathTool.ConstrainLatitude(b2);

            var grid = new Boundary { Minx = a, Miny = b, Maxx = a2, Maxy = b2 };
            grid.Normalize();
            return grid;
        }
        void MakeLines(JsonGetMarkersReceive jsonReceive)
        {
            if(!jsonReceive.IsDebugLinesEnabled) return; // client disabled it



            // Make the red lines data to be drawn in Google map
            
            var temp = new List<Rectangle>();

            const int borderLinesAdding = 1;
            var linesStepsX = (int)(Math.Round(Grid.AbsX / DeltaX) + borderLinesAdding);
            var linesStepsY = (int)(Math.Round(Grid.AbsY / DeltaY) + borderLinesAdding);

            var b = new Boundary(Grid);
            const double restrictLat = 5.5;
            b.Miny = MathTool.ConstrainLatitude(b.Miny, restrictLat); // Make sure it is visible on screen, restrict by some value
            b.Maxy = MathTool.ConstrainLatitude(b.Maxy, restrictLat);

            // Vertical lines
            for (var i = 0; i < linesStepsX; i++)
            {
                var xx  = b.Minx + i * DeltaX;
                
                // Draw region
                if (jsonReceive.Zoomlevel > 3)
                {
                    temp.Add(new Rectangle { Minx = xx, Miny = b.Miny, Maxx = xx, Maxy = b.Maxy });
                }
                // World wrap issue when same latlon area visible multiple times
                // Make sure line is drawn from left to right on screen
                else
                {
                    temp.Add(new Rectangle { Minx = xx, Miny = LatLonInfo.MinLatValue + restrictLat, Maxx = xx, Maxy = 0 });
                    temp.Add(new Rectangle { Minx = xx, Miny = 0, Maxx = xx, Maxy = LatLonInfo.MaxLatValue-restrictLat });
                }

            }

            // Horizontal lines            
            for (var i = 0; i < linesStepsY; i++)
            {
                var yy = b.Miny + i * DeltaY;
                                
                // Draw region
                if (jsonReceive.Zoomlevel > 3)
                {
                    // Don't draw lines outsize the world
                    if (MathTool.IsLowerThanLatMin(yy) || MathTool.IsGreaterThanLatMax(yy)) continue;

                    temp.Add(new Rectangle { Minx = b.Minx, Miny = yy, Maxx = b.Maxx, Maxy = yy });
                }                
                // World wrap issue when same latlon area visible multiple times
                // Make sure line is drawn from left to right on screen
                else
                {
                    temp.Add(new Rectangle { Minx = LatLonInfo.MinLonValue, Miny = yy, Maxx = 0, Maxy = yy });
                    temp.Add(new Rectangle { Minx = 0, Miny = yy, Maxx = LatLonInfo.MaxLonValue, Maxy = yy });
                }
            }

            // Normalize the lines and add as string
            foreach (var line in temp)
            {
                var x = (line.Minx).NormalizeLongitude().DoubleToString();
                var x2 = (line.Maxx).NormalizeLongitude().DoubleToString();
                var y = (line.Miny).NormalizeLatitude().DoubleToString();
                var y2 = (line.Maxy).NormalizeLatitude().DoubleToString();                
                Lines.Add(new Line { X = x, Y = y, X2 = x2, Y2 = y2 });
            }
        }
        // To work properly it requires the p is already normalized
        public static int[] GetPointMappedIds(IP p, Boundary grid, double deltax, double deltay)
        {
            var relativeX = p.X - grid.Minx;
            var relativeY = p.Y - grid.Miny;
            int idx, idy;

            // Naive version, lon points near 180 and lat points near 90 are not clustered together
            //idx = (int)(relativeX / deltax);
            //idy = (int)(relativeY / deltay);
            // end Naive version

            /*
            You have to draw a line with longitude values 180, -180 on papir to understand this            
                
             e.g. _deltaX = 20
longitude        150   170  180  -170   -150
                 |      |          |     |
                 
       
   idx =         7      8    9    -9    -8
                            -10    
                                  
here we want idx 8, 9, -10 and -9 be equal to each other, we set them to idx=8
then the longitudes from 170 to -170 will be clustered together
             */

            var overlapMapMinX = (int)(LatLonInfo.MinLonValue / deltax) - 1;
            var overlapMapMaxX = (int)(LatLonInfo.MaxLonValue / deltax);

            // The deltaX = 20 example scenario, then set the value 9 to 8 and -10 to -9            

            // Similar to if (LatLonInfo.MaxLonValue % deltax == 0) without floating presicion issue
            if (Math.Abs(LatLonInfo.MaxLonValue % deltax - 0) < Numbers.Epsilon)
            {
                overlapMapMaxX--;
                overlapMapMinX++;
            }

            var idxx = (int)(p.X / deltax);
            if (p.X < 0) idxx--;

            if (Math.Abs(LatLonInfo.MaxLonValue % p.X - 0) < Numbers.Epsilon)
            {
                if (p.X < 0) idxx++;
                else idxx--;
            }

            if (idxx == overlapMapMinX) idxx = overlapMapMaxX;

            idx = idxx;

            // Latitude never wraps around with Google Maps, ignore 90, -90 wrap-around for latitude
            idy = (int)(relativeY / deltay);

            return new[] { idx, idy };
        }
 public static bool IsInside(Boundary b, IP p)
 {
     return IsInside(b.Minx, b.Miny, b.Maxx, b.Maxy, p.X, p.Y, false, false);
 }
 // O(n), could be O(logn-ish) using range search or similar, no problem when points are <500.000
 public static IPoints FilterDataset(IPoints dataset, Boundary viewport)
 {
     //return new Points { Data = dataset.Data.Where(i => MathTool.IsInsideWiden(viewport, i)).ToList() };
     return new Points {Data = dataset.Data.Where(i => MathTool.IsInside(viewport, i)).ToList()};
 }
        public IPoints GetClusterResult(Boundary grid)
        {
            // Collect used buckets and return the result
            var clusterPoints = new Points();

            //O(m*n)
            foreach (var item in BucketsLookup)
            {
                var bucket = item.Value;
                if (!bucket.IsUsed) continue;

                if (bucket.Points.Count < AlgoConfig.MinClusterSize) clusterPoints.Data.AddRange(bucket.Points.Data);
                else
                {
                    bucket.Centroid.C = bucket.Points.Count;
                    clusterPoints.Add(bucket.Centroid);
                }
            }

            //var filtered = FilterDataset(clusterPoints, grid); // post filter data for client viewport
            //return filtered; //not working properly when zoomed far out.
            return clusterPoints;  // return not post filtered
        }
        static void ReadSerializedFileAndCount()
        {
            var points = FileUtil.LoadDataSetFromFile();
            var b = new Boundary { Minx = -179, Maxx = 179, Miny = -90, Maxy = 90 };
            int i = points.Data.Count(p => MathTool.IsInside(b, p));

            CWF("count: {0}", i);
        }
        static void PMapTest()
        {
            var b = new Boundary { Minx = -180, Maxx = 180, Miny = -90, Maxy = 90 };
            const int dx = 20;
            const int dy = 20;

            var xy = GridCluster.GetPointMappedIds(new P { X = 175, Y = 35 }, b, dx, dy);
            CWF("x: {0}   y: {1}", xy[0], xy[1]);

            xy = GridCluster.GetPointMappedIds(new P { X = 175, Y = 35 }, b, dx, dy);
            CWF("x: {0}   y: {1}", xy[0], xy[1]);

            xy = GridCluster.GetPointMappedIds(new P { X = 180, Y = 35 }, b, dx, dy);
            CWF("x: {0}   y: {1}", xy[0], xy[1]);

            xy = GridCluster.GetPointMappedIds((new P { X = 181, Y = 35 }).Normalize(), b, dx, dy);
            CWF("x: {0}   y: {1}", xy[0], xy[1]);

            xy = GridCluster.GetPointMappedIds((new P { X = -181, Y = 35 }).Normalize(), b, dx, dy);
            CWF("x: {0}   y: {1}", xy[0], xy[1]);
        }