text: 'use strict';
  
  var util = require('../util/util');
  var Evented = require('../util/evented');
  var TilePyramid = require('./tile_pyramid');
  var Source = require('./source');
  var urlResolve = require('resolve-url');
  var EXTENT = require('../data/bucket').EXTENT;
  var now = require('../util/browser').now;
  
  module.exports = GeoJSONSource;
  
  /**
   * Create a GeoJSON data source instance given an options object
   * @class GeoJSONSource
   * @param {Object} [options]
   * @param {Object|string} options.data A GeoJSON data object or URL to it. The latter is preferable in case of large GeoJSON files.
   * @param {number} [options.maxzoom=18] Maximum zoom to preserve detail at.
   * @param {number} [options.buffer] Tile buffer on each side in pixels.
   * @param {number} [options.tolerance] Simplification tolerance (higher means simpler) in pixels.
   * @param {number} [options.cluster] If the data is a collection of point features, setting this to true clusters the points by radius into groups.
   * @param {number} [options.clusterRadius=50] Radius of each cluster when clustering points, in pixels.
   * @param {number} [options.clusterMaxZoom] Max zoom to cluster points on. Defaults to one zoom less than `maxzoom` (so that last zoom features are not clustered).
  
   * @example
   * var sourceObj = new mapboxgl.GeoJSONSource({
   *    data: {
   *        "type": "FeatureCollection",
   *        "features": [{
   *            "type": "Feature",
   *            "geometry": {
   *                "type": "Point",
   *                "coordinates": [
   *                    -76.53063297271729,
   *                    39.18174077994108
   *                ]
   *            }
   *        }]
   *    }
   * });
   * map.addSource('some id', sourceObj); // add
   * map.removeSource('some id');  // remove
   */
  function GeoJSONSource(options) {
      options = options || {};
  
      this._data = options.data;
  
      if (options.maxzoom !== undefined) this.maxzoom = options.maxzoom;
  
      var scale = EXTENT / this.tileSize;
  
      this.geojsonVtOptions = {
          buffer: (options.buffer !== undefined ? options.buffer : 128) * scale,
          tolerance: (options.tolerance !== undefined ? options.tolerance : 0.375) * scale,
          extent: EXTENT,
          maxZoom: this.maxzoom
      };
  
      this.cluster = options.cluster || false;
      this.superclusterOptions = {
          maxZoom: Math.min(options.clusterMaxZoom, this.maxzoom - 1) || (this.maxzoom - 1),
          extent: EXTENT,
          radius: (options.clusterRadius || 50) * scale,
          log: false
      };
  
      this._pyramid = new TilePyramid({
          tileSize: this.tileSize,
          minzoom: this.minzoom,
          maxzoom: this.maxzoom,
          reparseOverscaled: true,
          load: this._loadTile.bind(this),
          abort: this._abortTile.bind(this),
          unload: this._unloadTile.bind(this),
          add: this._addTile.bind(this),
          remove: this._removeTile.bind(this),
          redoPlacement: this._redoTilePlacement.bind(this)
      });
      this.storedTiles = [];
      this.lastRefresh = now();
  }
  
  GeoJSONSource.prototype = util.inherit(Evented, /** @lends GeoJSONSource.prototype */{
      minzoom: 0,
      maxzoom: 18,
      tileSize: 512,
      _dirty: true,
      isTileClipped: true,
  
      /**
       * Update source geojson data and rerender map
       *
       * @param {Object|string} data A GeoJSON data object or URL to it. The latter is preferable in case of large GeoJSON files.
       * @returns {GeoJSONSource} this
       */
      setData: function(data) {
          this._data = data;
          this._dirty = true;
  
          this.fire('change');
  
          if (this.map)
              this.update(this.map.transform);
  
          return this;
      },
  
      onAdd: function(map) {
          this.map = map;
      },
  
      animate: function(delay, transitionStyle) {
          if (delay) {
              this.animated = true;
              this.delay = delay;
              switch (transitionStyle) {
              case 'none':
                  this.transitionStyle = 0;
                  break;
              case 'linear':
                  this.transitionStyle = 1;
                  break;
              case 'bezier':
                  this.transitionStyle = 2;
                  break;
              case 'midpoints':
                  this.transitionStyle = 3;
                  break;
              case 'circle':
                  this.transitionStyle = 4;
                  break;
              default:
                  this.transitionStyle = 1;
                  break;
              }
          } else {
              this.animated = false;
              this.transitionStyle = 0;
          }
          return this;
      },
  
      loaded: function() {
          return this._loaded && this._pyramid.loaded();
      },
  
      update: function(transform) {
          if (this._dirty) {
              this._updateData();
          }
  
          if (this._loaded) {
              this._pyramid.update(this.used, transform);
          }
      },
  
      reload: function() {
          if (this._loaded) {
              this._pyramid.reload();
          }
      },
  
      serialize: function() {
          return {
              type: 'geojson',
              data: this._data
          };
      },
  
      getVisibleCoordinates: Source._getVisibleCoordinates,
      getTile: Source._getTile,
  
      queryRenderedFeatures: Source._queryRenderedVectorFeatures,
      querySourceFeatures: Source._querySourceFeatures,
  
      _updateData: function() {
          this._dirty = false;
          var options = {
              tileSize: this.tileSize,
              source: this.id,
              geojsonVtOptions: this.geojsonVtOptions,
              cluster: this.cluster,
              superclusterOptions: this.superclusterOptions
          };
  
          var data = this._data;
          if (typeof data === 'string') {
              options.url = typeof window != 'undefined' ? urlResolve(window.location.href, data) : data;
          } else {
              options.data = JSON.stringify(data);
          }
          this.workerID = this.dispatcher.send('parse geojson', options, function(err) {
              this._loaded = true;
              if (err) {
                  this.fire('error', {error: err});
              } else {
                  this._pyramid.reload();
                  this.fire('change');
                  this.tileCount = this._pyramid.tileCount;
              }
  
          }.bind(this));
      },
  
      _loadTile: function(tile) {
          var overscaling = tile.coord.z > this.maxzoom ? Math.pow(2, tile.coord.z - this.maxzoom) : 1;
          var params = {
              uid: tile.uid,
              coord: tile.coord,
              zoom: tile.coord.z,
              maxZoom: this.maxzoom,
              tileSize: this.tileSize,
              source: this.id,
              overscaling: overscaling,
              angle: this.map.transform.angle,
              pitch: this.map.transform.pitch,
              showCollisionBoxes: this.map.showCollisionBoxes
          };
  
          tile.workerID = this.dispatcher.send('load geojson tile', params, function(err, data) {
  
              if (tile.aborted)
                  return;
  
              if (err) {
                  this.fire('tile.error', {tile: tile});
                  return;
              }
  
              this._storePreparedTile(tile.uid, data);
              this.fire('tile.load', {tile: tile});
              this.tileCount--;
              if (this.tileCount <= 0) this._refreshTiles();
  
          }.bind(this), this.workerID);
      },
      _storePreparedTile: function(uid, data) {
          this.storedTiles[uid] = data;
      },
      _loadPreparedTile: function(tile) {
          var tileData = this.storedTiles[tile.uid];
          if (!tileData) return;
          tile.unloadVectorData(this.map.painter);
          tile.loadVectorData(tileData);
          if (tile.redoWhenDone) {
              tile.redoWhenDone = false;
              tile.redoPlacement(this);
          }
      },
      _refreshTiles: function() {
          this._pyramid._cache.reset();
          var pyramidTiles = this._pyramid._tiles;
          var tileKeys = Object.keys(pyramidTiles);
          var tileCount = tileKeys.length;
          var i;
          for (i = 0; i < tileCount; i++) {
              var tile = pyramidTiles[tileKeys[i]];
              this._loadPreparedTile(tile);
          }
          this.lastRefresh = now();
          if (this.animated) {
              this.map.animationLoop.set(this.delay);
              this.map._rerender();
          } else if (typeof this.map._render === "function") {
              this.map._render();
          }
          this.storedTiles = [];
      },
      _abortTile: function(tile) {
          tile.aborted = true;
      },
  
      _addTile: function(tile) {
          this.fire('tile.add', {tile: tile});
      },
  
      _removeTile: function(tile) {
          this.fire('tile.remove', {tile: tile});
      },
  
      _unloadTile: function(tile) {
          tile.unloadVectorData(this.map.painter);
          this.dispatcher.send('remove tile', { uid: tile.uid, source: this.id }, null, tile.workerID);
      },
  
      redoPlacement: Source.redoPlacement,
  
      _redoTilePlacement: function(tile) {
          tile.redoPlacement(this);
      }
  });
  