package com.algobase.share.maps;


public class MercatorProjection {

    public static final double EARTH_CIRCUMFERENCE = 40075016.686;
    public static final double LATITUDE_MAX = 85.05112877980659;
    public static final double LATITUDE_MIN = -LATITUDE_MAX;

  //int tileSize = 512;
    int tileSize = 256;

    long mapSize = tileSize;
    long tileMax = 0;

    int zoomLevel = 0;

    public long pixelSize() { return tileSize << zoomLevel; }
    public long tileSize() { return (long)1 << zoomLevel; }


    public MercatorProjection(int tile_sz) {
        tileSize = tile_sz;
        zoomLevel = 0;
        mapSize = tileSize << zoomLevel; 
        tileMax = (1L << zoomLevel) -1;
    }

    public void setTileSize(int tile_sz) {
       tileSize = tile_sz;
       mapSize = tileSize << zoomLevel; 
       tileMax = (1L << zoomLevel) -1;
    }

    public void setZoomLevel(int zlevel) {
        zoomLevel = zlevel;
        mapSize = tileSize << zoomLevel; 
        tileMax = (1L << zoomLevel) -1;
    }

    public int getZoomLevel() { return zoomLevel; }



   public long numberOfTiles(double lat1, double lat2,
                             double lon1, double lon2, int zLevel)
   { int zl = zoomLevel;
     setZoomLevel(zLevel);
     long y1 = pixelYToTileY(latitudeToPixelY(lat1));
     long y2 = pixelYToTileY(latitudeToPixelY(lat2));
     long x1 = pixelXToTileX(longitudeToPixelX(lon1));
     long x2 = pixelXToTileX(longitudeToPixelX(lon2));
     long num = (long)((Math.abs(y1-y2) + 1) * (Math.abs(y1-y2) + 1));
     setZoomLevel(zl);
     long sum = 0;
     while (--zLevel > 0) {
        sum += num;
        num /= 4;
     }
     return sum;
    }



    public double getResolution(double latitude) {
        // 1 pixel to lon degrees at given latitude
        double d = Math.cos(latitude * (Math.PI / 180)) * EARTH_CIRCUMFERENCE;
        return d / mapSize;
    }


    public double lonSpanToPixels(double lon_span, int zLevel) { 
      return (lon_span/360) * (tileSize << zLevel);
    }


    public double latSpanToPixels(double lat_span, int zLevel) 
    { int zl = zoomLevel;
      setZoomLevel(zLevel);
      double py1 = latitudeToPixelY(0);
      double py2 = latitudeToPixelY(lat_span);
      setZoomLevel(zl);
      return Math.abs(py1-py2);
    }


    public double deltaLat(double pixels, double lat, int zLevel) 
    { // pixel dist to lat degrees at given latitude and zoom level
      int zl = zoomLevel;
      setZoomLevel(zLevel);
      double py = latitudeToPixelY(lat);
      double d = Math.abs(pixelYToLatitude(py + pixels) - lat);
      setZoomLevel(zl);
      return d;
    }


    public double latitudeToPixelY(double latitude) 
    { double sinLat = Math.sin(latitude * (Math.PI / 180));
      double py = (0.5 - Math.log((1+sinLat) / (1-sinLat)) / (4*Math.PI)) * mapSize;
      if (py < 0) py = 0;
      if (py > mapSize) py = mapSize;
      return py;
    }

    public long latitudeToTileY(double latitude) {
       return pixelYToTileY(latitudeToPixelY(latitude));
    }

    public double longitudeToPixelX(double longitude) {
       return (longitude + 180) / 360 * mapSize;
    }

    public long longitudeToTileX(double longitude) {
       return pixelXToTileX(longitudeToPixelX(longitude));
    }


    public long pixelXToTileX(double px) {
       if (px < 0) px = 0;
       return (long)Math.min(px/tileSize, tileMax); 
    }

    public long pixelYToTileY(double py) {
       if (py < 0) py = 0;
       return (long)Math.min(py/tileSize, tileMax); 
    }


    public double pixelXToLongitude(double px) {
       if (px < 0) px = 0;
       if (px > mapSize) px = mapSize;
       return 360 * ((px / mapSize) - 0.5);
    }

    public double pixelYToLatitude(double py) {
       if (py < 0) py = 0;
       if (py > mapSize) py = mapSize;
       double y = 0.5 - (py / mapSize);
       return 90 - 360 * Math.atan(Math.exp(-y * (2 * Math.PI))) / Math.PI;
    }


    public double tileXToLongitude(long tileX) {
       return pixelXToLongitude(tileX * tileSize);
    }

    public double tileYToLatitude(long tileY) {
       return pixelYToLatitude(tileY * tileSize);
    }


    public float meterToPixels(float m) {
       return (float)(m*mapSize/EARTH_CIRCUMFERENCE);
    }

}

