
namespace GMap.NET.Projections
{
   using System;

   /// <summary>
   /// GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0,AUTHORITY[\"EPSG\",\"8901\"]],UNIT[\"degree\",0.01745329251994328,AUTHORITY[\"EPSG\",\"9122\"]],AUTHORITY[\"EPSG\",\"4326\"]]
   /// PROJCS["LKS92 / Latvia TM",GEOGCS["LKS92",DATUM["D_Latvia_1992",SPHEROID["GRS_1980",6378137,298.257222101]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",24],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",-6000000],UNIT["Meter",1]]
   /// </summary>
   public class LKS92Projection : PureProjection
   {
      public static readonly LKS92Projection Instance = new LKS92Projection();

      static readonly double MinLatitude = 55.55;
      static readonly double MaxLatitude = 58.58;
      static readonly double MinLongitude = 20.22;
      static readonly double MaxLongitude = 28.28;

      static readonly double orignX = 5120900;
      static readonly double orignY = 3998100;

      static readonly double scaleFactor = 0.9996;	                // scale factor				
      static readonly double centralMeridian = 0.41887902047863912;// Center longitude (projection center) 
      static readonly double latOrigin = 0.0;	                   // center latitude			
      static readonly double falseNorthing = -6000000.0;	          // y offset in meters			
      static readonly double falseEasting = 500000.0;	       // x offset in meters			
      static readonly double semiMajor = 6378137.0;		    // major axis
      static readonly double semiMinor = 6356752.3141403561; // minor axis
      static readonly double semiMinor2 = 6356752.3142451793;		// minor axis
      static readonly double metersPerUnit = 1.0;
      static readonly double COS_67P5 = 0.38268343236508977; // cosine of 67.5 degrees
      static readonly double AD_C = 1.0026000;               // Toms region 1 constant

      public override RectLatLng Bounds
      {
         get
         {
            return RectLatLng.FromLTRB(MinLongitude, MaxLatitude, MaxLongitude, MinLatitude);
         }
      }

      GSize tileSize = new GSize(256, 256);
      public override GSize TileSize
      {
         get
         {
            return tileSize;
         }
      }

      public override double Axis
      {
         get
         {
            return 6378137;
         }
      }

      public override double Flattening
      {
         get
         {
            return (1.0 / 298.257222101);
         }
      }

      public override GPoint FromLatLngToPixel(double lat, double lng, int zoom)
      {
         GPoint ret = GPoint.Empty;

         lat = Clip(lat, MinLatitude, MaxLatitude);
         lng = Clip(lng, MinLongitude, MaxLongitude);

         double[] lks = new double[] { lng, lat };
         lks = DTM10(lks);
         lks = MTD10(lks);
         lks = DTM00(lks);

         double res = GetTileMatrixResolution(zoom);

         ret.X = (long)Math.Floor((lks[0] + orignX) / res);
         ret.Y = (long)Math.Floor((orignY - lks[1]) / res);

         return ret;
      }

      public override PointLatLng FromPixelToLatLng(long x, long y, int zoom)
      {
         PointLatLng ret = PointLatLng.Empty;

         double res = GetTileMatrixResolution(zoom);

         double[] lks = new double[] { (x * res) - orignX, -(y * res) + orignY };
         lks = MTD11(lks);
         lks = DTM10(lks);
         lks = MTD10(lks);

         ret.Lat = Clip(lks[1], MinLatitude, MaxLatitude);
         ret.Lng = Clip(lks[0], MinLongitude, MaxLongitude);

         return ret;
      }

      double[] DTM10(double[] lonlat)
      {
         // Eccentricity squared : (a^2 - b^2)/a^2
         double es = 1.0 - (semiMinor2 * semiMinor2) / (semiMajor * semiMajor); // e^2

         // Second eccentricity squared : (a^2 - b^2)/b^2
         double ses = (Math.Pow(semiMajor, 2) - Math.Pow(semiMinor2, 2)) / Math.Pow(semiMinor2, 2);

         double ba = semiMinor2 / semiMajor;
         double ab = semiMajor / semiMinor2;

         double lon = DegreesToRadians(lonlat[0]);
         double lat = DegreesToRadians(lonlat[1]);
         double h = lonlat.Length < 3 ? 0 : lonlat[2].Equals(Double.NaN) ? 0 : lonlat[2];
         double v = semiMajor / Math.Sqrt(1 - es * Math.Pow(Math.Sin(lat), 2));
         double x = (v + h) * Math.Cos(lat) * Math.Cos(lon);
         double y = (v + h) * Math.Cos(lat) * Math.Sin(lon);
         double z = ((1 - es) * v + h) * Math.Sin(lat);
         return new double[] { x, y, z, };
      }

      double[] MTD10(double[] pnt)
      {
         // Eccentricity squared : (a^2 - b^2)/a^2
         double es = 1.0 - (semiMinor * semiMinor) / (semiMajor * semiMajor); // e^2

         // Second eccentricity squared : (a^2 - b^2)/b^2
         double ses = (Math.Pow(semiMajor, 2) - Math.Pow(semiMinor, 2)) / Math.Pow(semiMinor, 2);

         double ba = semiMinor / semiMajor;
         double ab = semiMajor / semiMinor;

         bool AtPole = false; // is location in polar region
         double Z = pnt.Length < 3 ? 0 : pnt[2].Equals(Double.NaN) ? 0 : pnt[2];

         double lon = 0;
         double lat = 0;
         double Height = 0;
         if(pnt[0] != 0.0)
         {
            lon = Math.Atan2(pnt[1], pnt[0]);
         }
         else
         {
            if(pnt[1] > 0)
            {
               lon = Math.PI / 2;
            }
            else
               if(pnt[1] < 0)
               {
                  lon = -Math.PI * 0.5;
               }
               else
               {
                  AtPole = true;
                  lon = 0.0;
                  if(Z > 0.0) // north pole
                  {
                     lat = Math.PI * 0.5;
                  }
                  else
                     if(Z < 0.0) // south pole
                     {
                        lat = -Math.PI * 0.5;
                     }
                     else // center of earth
                     {
                        return new double[] { RadiansToDegrees(lon), RadiansToDegrees(Math.PI * 0.5), -semiMinor, };
                     }
               }
         }
         double W2 = pnt[0] * pnt[0] + pnt[1] * pnt[1]; // Square of distance from Z axis
         double W = Math.Sqrt(W2); // distance from Z axis
         double T0 = Z * AD_C; // initial estimate of vertical component
         double S0 = Math.Sqrt(T0 * T0 + W2); // initial estimate of horizontal component
         double Sin_B0 = T0 / S0; // sin(B0), B0 is estimate of Bowring aux variable
         double Cos_B0 = W / S0; // cos(B0)
         double Sin3_B0 = Math.Pow(Sin_B0, 3);
         double T1 = Z + semiMinor * ses * Sin3_B0; // corrected estimate of vertical component
         double Sum = W - semiMajor * es * Cos_B0 * Cos_B0 * Cos_B0; // numerator of cos(phi1)
         double S1 = Math.Sqrt(T1 * T1 + Sum * Sum); // corrected estimate of horizontal component
         double Sin_p1 = T1 / S1; // sin(phi1), phi1 is estimated latitude
         double Cos_p1 = Sum / S1; // cos(phi1)
         double Rn = semiMajor / Math.Sqrt(1.0 - es * Sin_p1 * Sin_p1); // Earth radius at location
         if(Cos_p1 >= COS_67P5)
         {
            Height = W / Cos_p1 - Rn;
         }
         else
            if(Cos_p1 <= -COS_67P5)
            {
               Height = W / -Cos_p1 - Rn;
            }
            else
            {
               Height = Z / Sin_p1 + Rn * (es - 1.0);
            }

         if(!AtPole)
         {
            lat = Math.Atan(Sin_p1 / Cos_p1);
         }
         return new double[] { RadiansToDegrees(lon), RadiansToDegrees(lat), Height, };
      }

      double[] DTM00(double[] lonlat)
      {
         double e0, e1, e2, e3;	// eccentricity constants		
         double e, es, esp;		// eccentricity constants		
         double ml0;		         // small value m			

         es = 1.0 - Math.Pow(semiMinor / semiMajor, 2);
         e = Math.Sqrt(es);
         e0 = e0fn(es);
         e1 = e1fn(es);
         e2 = e2fn(es);
         e3 = e3fn(es);
         ml0 = semiMajor * mlfn(e0, e1, e2, e3, latOrigin);
         esp = es / (1.0 - es);

         // ...		

         double lon = DegreesToRadians(lonlat[0]);
         double lat = DegreesToRadians(lonlat[1]);

         double delta_lon = 0.0;  // Delta longitude (Given longitude - center)
         double sin_phi, cos_phi; // sin and cos value				
         double al, als;		    // temporary values				
         double c, t, tq;	       // temporary values				
         double con, n, ml;	    // cone constant, small m			

         delta_lon = AdjustLongitude(lon - centralMeridian);
         SinCos(lat, out sin_phi, out cos_phi);

         al = cos_phi * delta_lon;
         als = Math.Pow(al, 2);
         c = esp * Math.Pow(cos_phi, 2);
         tq = Math.Tan(lat);
         t = Math.Pow(tq, 2);
         con = 1.0 - es * Math.Pow(sin_phi, 2);
         n = semiMajor / Math.Sqrt(con);
         ml = semiMajor * mlfn(e0, e1, e2, e3, lat);

         double x = scaleFactor * n * al * (1.0 + als / 6.0 * (1.0 - t + c + als / 20.0 *
             (5.0 - 18.0 * t + Math.Pow(t, 2) + 72.0 * c - 58.0 * esp))) + falseEasting;

         double y = scaleFactor * (ml - ml0 + n * tq * (als * (0.5 + als / 24.0 *
             (5.0 - t + 9.0 * c + 4.0 * Math.Pow(c, 2) + als / 30.0 * (61.0 - 58.0 * t
             + Math.Pow(t, 2) + 600.0 * c - 330.0 * esp))))) + falseNorthing;

         if(lonlat.Length < 3)
            return new double[] { x / metersPerUnit, y / metersPerUnit };
         else
            return new double[] { x / metersPerUnit, y / metersPerUnit, lonlat[2] };
      }

      double[] DTM01(double[] lonlat)
      {
         // Eccentricity squared : (a^2 - b^2)/a^2
         double es = 1.0 - (semiMinor * semiMinor) / (semiMajor * semiMajor);

         // Second eccentricity squared : (a^2 - b^2)/b^2
         double ses = (Math.Pow(semiMajor, 2) - Math.Pow(semiMinor, 2)) / Math.Pow(semiMinor, 2);

         double ba = semiMinor / semiMajor;
         double ab = semiMajor / semiMinor;

         double lon = DegreesToRadians(lonlat[0]);
         double lat = DegreesToRadians(lonlat[1]);
         double h = lonlat.Length < 3 ? 0 : lonlat[2].Equals(Double.NaN) ? 0 : lonlat[2];
         double v = semiMajor / Math.Sqrt(1 - es * Math.Pow(Math.Sin(lat), 2));
         double x = (v + h) * Math.Cos(lat) * Math.Cos(lon);
         double y = (v + h) * Math.Cos(lat) * Math.Sin(lon);
         double z = ((1 - es) * v + h) * Math.Sin(lat);
         return new double[] { x, y, z, };
      }

      double[] MTD01(double[] pnt)
      {
         // Eccentricity squared : (a^2 - b^2)/a^2
         double es = 1.0 - (semiMinor2 * semiMinor2) / (semiMajor * semiMajor);

         // Second eccentricity squared : (a^2 - b^2)/b^2
         double ses = (Math.Pow(semiMajor, 2) - Math.Pow(semiMinor2, 2)) / Math.Pow(semiMinor2, 2);

         double ba = semiMinor2 / semiMajor;
         double ab = semiMajor / semiMinor2;

         bool At_Pole = false; // is location in polar region
         double Z = pnt.Length < 3 ? 0 : pnt[2].Equals(Double.NaN) ? 0 : pnt[2];

         double lon = 0;
         double lat = 0;
         double Height = 0;
         if(pnt[0] != 0.0)
         {
            lon = Math.Atan2(pnt[1], pnt[0]);
         }
         else
         {
            if(pnt[1] > 0)
            {
               lon = Math.PI / 2;
            }
            else
               if(pnt[1] < 0)
               {
                  lon = -Math.PI * 0.5;
               }
               else
               {
                  At_Pole = true;
                  lon = 0.0;
                  if(Z > 0.0) // north pole
                  {
                     lat = Math.PI * 0.5;
                  }
                  else
                     if(Z < 0.0) // south pole
                     {
                        lat = -Math.PI * 0.5;
                     }
                     else // center of earth
                     {
                        return new double[] { RadiansToDegrees(lon), RadiansToDegrees(Math.PI * 0.5), -semiMinor2, };
                     }
               }
         }

         double W2 = pnt[0] * pnt[0] + pnt[1] * pnt[1]; // Square of distance from Z axis
         double W = Math.Sqrt(W2);                      // distance from Z axis
         double T0 = Z * AD_C;                // initial estimate of vertical component
         double S0 = Math.Sqrt(T0 * T0 + W2); //initial estimate of horizontal component
         double Sin_B0 = T0 / S0;             // sin(B0), B0 is estimate of Bowring aux variable
         double Cos_B0 = W / S0;              // cos(B0)
         double Sin3_B0 = Math.Pow(Sin_B0, 3);
         double T1 = Z + semiMinor2 * ses * Sin3_B0; //corrected estimate of vertical component
         double Sum = W - semiMajor * es * Cos_B0 * Cos_B0 * Cos_B0; // numerator of cos(phi1)
         double S1 = Math.Sqrt(T1 * T1 + Sum * Sum); // corrected estimate of horizontal component
         double Sin_p1 = T1 / S1;  // sin(phi1), phi1 is estimated latitude
         double Cos_p1 = Sum / S1; // cos(phi1)
         double Rn = semiMajor / Math.Sqrt(1.0 - es * Sin_p1 * Sin_p1); // Earth radius at location

         if(Cos_p1 >= COS_67P5)
         {
            Height = W / Cos_p1 - Rn;
         }
         else
            if(Cos_p1 <= -COS_67P5)
            {
               Height = W / -Cos_p1 - Rn;
            }
            else
            {
               Height = Z / Sin_p1 + Rn * (es - 1.0);
            }

         if(!At_Pole)
         {
            lat = Math.Atan(Sin_p1 / Cos_p1);
         }
         return new double[] { RadiansToDegrees(lon), RadiansToDegrees(lat), Height, };
      }

      double[] MTD11(double[] p)
      {
         double e0, e1, e2, e3;	// eccentricity constants		
         double e, es, esp;		// eccentricity constants		
         double ml0;		    // small value m

         es = 1.0 - Math.Pow(semiMinor / semiMajor, 2);
         e = Math.Sqrt(es);
         e0 = e0fn(es);
         e1 = e1fn(es);
         e2 = e2fn(es);
         e3 = e3fn(es);
         ml0 = semiMajor * mlfn(e0, e1, e2, e3, latOrigin);
         esp = es / (1.0 - es);

         // ...

         double con, phi;
         double delta_phi;
         long i;
         double sin_phi, cos_phi, tan_phi;
         double c, cs, t, ts, n, r, d, ds;
         long max_iter = 6;

         double x = p[0] * metersPerUnit - falseEasting;
         double y = p[1] * metersPerUnit - falseNorthing;

         con = (ml0 + y / scaleFactor) / semiMajor;
         phi = con;
         for(i = 0; ; i++)
         {
            delta_phi = ((con + e1 * Math.Sin(2.0 * phi) - e2 * Math.Sin(4.0 * phi) + e3 * Math.Sin(6.0 * phi)) / e0) - phi;
            phi += delta_phi;

            if(Math.Abs(delta_phi) <= EPSLoN)
               break;

            if(i >= max_iter)
               throw new ArgumentException("Latitude failed to converge");
         }

         if(Math.Abs(phi) < HALF_PI)
         {
            SinCos(phi, out sin_phi, out cos_phi);
            tan_phi = Math.Tan(phi);
            c = esp * Math.Pow(cos_phi, 2);
            cs = Math.Pow(c, 2);
            t = Math.Pow(tan_phi, 2);
            ts = Math.Pow(t, 2);
            con = 1.0 - es * Math.Pow(sin_phi, 2);
            n = semiMajor / Math.Sqrt(con);
            r = n * (1.0 - es) / con;
            d = x / (n * scaleFactor);
            ds = Math.Pow(d, 2);

            double lat = phi - (n * tan_phi * ds / r) * (0.5 - ds / 24.0 * (5.0 + 3.0 * t +
                10.0 * c - 4.0 * cs - 9.0 * esp - ds / 30.0 * (61.0 + 90.0 * t +
                298.0 * c + 45.0 * ts - 252.0 * esp - 3.0 * cs)));

            double lon = AdjustLongitude(centralMeridian + (d * (1.0 - ds / 6.0 * (1.0 + 2.0 * t +
                c - ds / 20.0 * (5.0 - 2.0 * c + 28.0 * t - 3.0 * cs + 8.0 * esp +
                24.0 * ts))) / cos_phi));

            if(p.Length < 3)
               return new double[] { RadiansToDegrees(lon), RadiansToDegrees(lat) };
            else
               return new double[] { RadiansToDegrees(lon), RadiansToDegrees(lat), p[2] };
         }
         else
         {
            if(p.Length < 3)
               return new double[] { RadiansToDegrees(HALF_PI * Sign(y)), RadiansToDegrees(centralMeridian) };
            else
               return new double[] { RadiansToDegrees(HALF_PI * Sign(y)), RadiansToDegrees(centralMeridian), p[2] };
         }
      }

      #region -- levels info --
      //"spatialReference":{"wkid":3059},"singleFusedMapCache":true,"tileInfo":{"rows":256,"cols":256,"dpi":96,"format":"PNG8","compressionQuality":0,
      //"origin":{"x":-5120900,"y":3998100},"spatialReference":{"wkid":3059},

      //"lods":[{
      //"level":0,"resolution":1587.50317500635,"scale":6000000},
      //{"level":1,"resolution":793.751587503175,"scale":3000000},
      //{"level":2,"resolution":529.167725002117,"scale":2000000},
      //{"level":3,"resolution":264.583862501058,"scale":1000000},
      //{"level":4,"resolution":132.291931250529,"scale":500000},
      //{"level":5,"resolution":52.9167725002117,"scale":200000},
      //{"level":6,"resolution":26.4583862501058,"scale":100000},
      //{"level":7,"resolution":13.2291931250529,"scale":50000},
      //{"level":8,"resolution":6.61459656252646,"scale":25000},
      //{"level":9,"resolution":2.64583862501058,"scale":10000},
      //{"level":10,"resolution":1.32291931250529,"scale":5000},
      //{"level":11,"resolution":0.529167725002117,"scale":2000}]},

      //"initialExtent":{"xmin":290284.5745,"ymin":159644.05,"xmax":785045.1155,"ymax":452176.95,"spatialReference":{"wkid":3059}},
      //"fullExtent":{"xmin":290284.5745,"ymin":159644.05,"xmax":785045.1155,"ymax":452176.95,"spatialReference":{"wkid":3059}},

      //"units":"esriMeters","supportedImageFormatTypes":"PNG24,PNG,JPG,DIB,TIFF,EMF,PS,PDF,GIF,SVG,SVGZ,AI,BMP","documentInfo":{"Title":"ikartelv","Author":"gstanevicius","Comments":"","Subject":"","Category":"","Keywords":"","Credits":""},"capabilities":"Map,Query,Data"}); 
      #endregion

      public static double GetTileMatrixResolution(int zoom)
      {
         double ret = 0;

         switch(zoom)
         {
            #region -- sizes --
            case 0:
            {
               ret = 1587.50317500635;
            }
            break;

            case 1:
            {
               ret = 793.751587503175;
            }
            break;

            case 2:
            {
               ret = 529.167725002117;
            }
            break;

            case 3:
            {
               ret = 264.583862501058;
            }
            break;

            case 4:
            {
               ret = 132.291931250529;
            }
            break;

            case 5:
            {
               ret = 52.9167725002117;
            }
            break;

            case 6:
            {
               ret = 26.4583862501058;
            }
            break;

            case 7:
            {
               ret = 13.2291931250529;
            }
            break;

            case 8:
            {
               ret = 6.61459656252646;
            }
            break;

            case 9:
            {
               ret = 2.64583862501058;
            }
            break;

            case 10:
            {
               ret = 1.32291931250529;
            }
            break;

            case 11:
            {
               ret = 0.529167725002117;
            }
            break;
            #endregion
         }

         return ret;
      }

      public override double GetGroundResolution(int zoom, double latitude)
      {
         return GetTileMatrixResolution(zoom);
      }

      public override GSize GetTileMatrixMinXY(int zoom)
      {
         GSize ret = GSize.Empty;

         switch(zoom)
         {
            #region -- sizes --
            case 0:
            {
               ret = new GSize(13, 8);
            }
            break;

            case 1:
            {
               ret = new GSize(26, 17);
            }
            break;

            case 2:
            {
               ret = new GSize(39, 26);
            }
            break;

            case 3:
            {
               ret = new GSize(79, 52);
            }
            break;

            case 4:
            {
               ret = new GSize(159, 105);
            }
            break;

            case 5:
            {
               ret = new GSize(399, 262);
            }
            break;

            case 6:
            {
               ret = new GSize(798, 525);
            }
            break;

            case 7:
            {
               ret = new GSize(1597, 1050);
            }
            break;

            case 8:
            {
               ret = new GSize(3195, 2101);
            }
            break;

            case 9:
            {
               ret = new GSize(7989, 5254);
            }
            break;

            case 10:
            {
               ret = new GSize(15978, 10509);
            }
            break;

            case 11:
            {
               ret = new GSize(39945, 26273);
            }
            break;
            #endregion
         }

         return ret;
      }

      public override GSize GetTileMatrixMaxXY(int zoom)
      {
         GSize ret = GSize.Empty;

         switch(zoom)
         {
            #region -- sizes --
            case 0:
            {
               ret = new GSize(14, 9);
            }
            break;

            case 1:
            {
               ret = new GSize(28, 18);
            }
            break;

            case 2:
            {
               ret = new GSize(43, 28);
            }
            break;

            case 3:
            {
               ret = new GSize(86, 56);
            }
            break;

            case 4:
            {
               ret = new GSize(173, 112);
            }
            break;

            case 5:
            {
               ret = new GSize(434, 282);
            }
            break;

            case 6:
            {
               ret = new GSize(868, 564);
            }
            break;

            case 7:
            {
               ret = new GSize(1737, 1129);
            }
            break;

            case 8:
            {
               ret = new GSize(3474, 2258);
            }
            break;

            case 9:
            {
               ret = new GSize(8686, 5647);
            }
            break;

            case 10:
            {
               ret = new GSize(17372, 11294);
            }
            break;

            case 11:
            {
               ret = new GSize(43430, 28236);
            }
            break;
            #endregion
         }

         return ret;
      }
   }
}
