text: 'use strict';
  
  var LatLng = require('./lat_lng'),
      Point = require('point-geometry'),
      wrap = require('../util/util').wrap;
  
  module.exports = Transform;
  
  // A single transform, generally used for a single tile to be scaled, rotated, and zoomed.
  
  function Transform(minZoom, maxZoom) {
      this.tileSize = 512; // constant
  
      this._minZoom = minZoom || 0;
      this._maxZoom = maxZoom || 22;
  
      this.latRange = [-85.05113, 85.05113];
  
      this.width = 0;
      this.height = 0;
      this.zoom = 0;
      this.center = new LatLng(0, 0);
      this.angle = 0;
  }
  
  Transform.prototype = {
      get minZoom() { return this._minZoom; },
      set minZoom(zoom) {
          this._minZoom = zoom;
          this.zoom = Math.max(this.zoom, zoom);
      },
  
      get maxZoom() { return this._maxZoom; },
      set maxZoom(zoom) {
          this._maxZoom = zoom;
          this.zoom = Math.min(this.zoom, zoom);
      },
  
      get worldSize() {
          return this.tileSize * this.scale;
      },
  
      get centerPoint() {
          return this.size._div(2);
      },
  
      get size() {
          return new Point(this.width, this.height);
      },
  
      get bearing() {
          return -this.angle / Math.PI * 180;
      },
      set bearing(bearing) {
          this.angle = -wrap(bearing, -180, 180) * Math.PI / 180;
      },
  
      get zoom() { return this._zoom; },
      set zoom(zoom) {
          zoom = Math.min(Math.max(zoom, this.minZoom), this.maxZoom);
          this._zoom = zoom;
          this.scale = this.zoomScale(zoom);
          this.tileZoom = Math.floor(zoom);
          this.zoomFraction = zoom - this.tileZoom;
          this._constrain();
      },
  
      zoomScale(zoom) { return Math.pow(2, zoom); },
      scaleZoom(scale) { return Math.log(scale) / Math.LN2; },
  
      project(latlng, worldSize) {
          return new Point(
              this.lngX(latlng.lng, worldSize),
              this.latY(latlng.lat, worldSize));
      },
  
      unproject(point, worldSize) {
          return new LatLng(
              this.yLat(point.y, worldSize),
              this.xLng(point.x, worldSize));
      },
  
      get x() { return this.lngX(this.center.lng); },
      get y() { return this.latY(this.center.lat); },
  
      get point() { return new Point(this.x, this.y); },
  
      // lat/lon <-> absolute pixel coords convertion
      lngX(lon, worldSize) {
          return (180 + lon) * (worldSize || this.worldSize) / 360;
      },
      // latitude to absolute y coord
      latY(lat, worldSize) {
          var y = 180 / Math.PI * Math.log(Math.tan(Math.PI / 4 + lat * Math.PI / 360));
          return (180 - y) * (worldSize || this.worldSize) / 360;
      },
  
      xLng(x, worldSize) {
          return x * 360 / (worldSize || this.worldSize) - 180;
      },
      yLat(y, worldSize) {
          var y2 = 180 - y * 360 / (worldSize || this.worldSize);
          return 360 / Math.PI * Math.atan(Math.exp(y2 * Math.PI / 180)) - 90;
      },
  
      panBy(offset) {
          var point = this.centerPoint._add(offset);
          this.center = this.pointLocation(point);
          this._constrain();
      },
  
      setZoomAround(zoom, center) {
          var p = this.locationPoint(center),
              p1 = this.size._sub(p),
              latlng = this.pointLocation(p1);
          this.zoom = zoom;
          this.panBy(p1.sub(this.locationPoint(latlng)));
      },
  
      setBearingAround(bearing, center) {
          var offset = this.locationPoint(center).sub(this.centerPoint);
          this.panBy(offset);
          this.bearing = bearing;
          this.panBy(offset.mult(-1));
      },
  
      locationPoint(latlng) {
          var p = this.project(latlng);
          return this.centerPoint._sub(this.point._sub(p)._rotate(this.angle));
      },
  
      pointLocation(p) {
          var p2 = this.centerPoint._sub(p)._rotate(-this.angle);
          return this.unproject(this.point.sub(p2));
      },
  
      locationCoordinate(latlng) {
          var k = this.zoomScale(this.tileZoom) / this.worldSize;
          return {
              column: this.lngX(latlng.lng) * k,
              row: this.latY(latlng.lat) * k,
              zoom: this.tileZoom
          };
      },
  
      pointCoordinate(tileCenter, p) {
          var zoomFactor = this.zoomScale(this.zoomFraction),
              kt = this.zoomScale(this.tileZoom - tileCenter.zoom),
              p2 = this.centerPoint._sub(p)._rotate(-this.angle)._div(this.tileSize * zoomFactor);
  
          return {
              column: tileCenter.column * kt - p2.x,
              row: tileCenter.row * kt - p2.y,
              zoom: this.tileZoom
          };
      },
  
      _constrain() {
          if (!this.center) return;
  
          var minY, maxY, minX, maxX, sy, sx, x2, y2,
              size = this.size;
  
          if (this.latRange) {
              minY = this.latY(this.latRange[1]);
              maxY = this.latY(this.latRange[0]);
              sy = maxY - minY < size.y ? size.y / (maxY - minY) : 0;
          }
  
          if (this.lngRange) {
              minX = this.lngX(this.lngRange[0]);
              maxX = this.lngX(this.lngRange[1]);
              sx = maxX - minX < size.x ? size.x / (maxX - minX) : 0;
          }
  
          // how much the map should scale to fit the screen into given latitude/longitude ranges
          var s = Math.max(sx || 0, sy || 0);
  
          if (s) {
              this.center = this.unproject(new Point(
                  sx ? (maxX + minX) / 2 : this.x,
                  sy ? (maxY + minY) / 2 : this.y));
              this.zoom += this.scaleZoom(s);
              return;
          }
  
          if (this.latRange) {
              var y = this.y,
                  h2 = size.y / 2;
  
              if (y - h2 < minY) y2 = minY + h2;
              if (y + h2 > maxY) y2 = maxY - h2;
          }
  
          if (this.lngRange) {
              var x = this.x,
                  w2 = size.x / 2;
  
              if (x - w2 < minX) x2 = minX + w2;
              if (x + w2 > maxX) x2 = maxX - w2;
          }
  
          // pan the map if the screen goes off the range
          if (x2 !== undefined || y2 !== undefined) {
              this.center = this.unproject(new Point(
                  x2 !== undefined ? x2 : this.x,
                  y2 !== undefined ? y2 : this.y));
          }
      }
  };
  