text: import Layer from './Layer';
  import extend from 'lodash.assign';
  import reqwest from 'reqwest';
  import GeoJSON from '../util/GeoJSON';
  import Worker from '../util/Worker';
  import Buffer from '../util/Buffer';
  import Stringify from '../util/Stringify';
  import PolygonLayer from './geometry/PolygonLayer';
  import PolylineLayer from './geometry/PolylineLayer';
  import PointLayer from './geometry/PointLayer';
  import {latLon as LatLon} from '../geo/LatLon';
  import {point as Point} from '../geo/Point';
  import Geo from '../geo/Geo';
  import PickingMaterial from '../engine/PickingMaterial';
  
  // TODO: Allow filter method to be run inside a worker to improve performance
  // TODO: Allow onEachFeature method to be run inside a worker to improve performance
  
  class GeoJSONWorkerLayer extends Layer {
    constructor(geojson, options) {
      var defaults = {
        topojson: false,
        style: GeoJSON.defaultStyle,
        onEachFeature: null,
        onEachFeatureWorker: null,
        onAddAttributes: null,
        interactive: false,
        pointGeometry: null,
        onClick: null,
        headers: {}
      };
  
      var _options = extend({}, defaults, options);
  
      if (typeof options.style === 'object') {
        _options.style = extend({}, defaults.style, options.style);
      }
  
      super(_options);
  
      this._aborted = false;
      this._geojson = geojson;
    }
  
    _onAdd(world) {
      if (this._options.interactive) {
        // Worker layer always controls output to add a picking mesh
        this._pickingMesh = new THREE.Object3D();
      }
  
      // Process GeoJSON
      return this._process(this._geojson).catch((err) => {
        console.error(err);
      });
    }
  
    // Use workers to request and process GeoJSON, returning data structure
    // containing geometry and any supplementary data for output
    _process(_geojson) {
      return new Promise((resolve, reject) => {
        var style = this._options.style;
  
        // TODO: Convert to buffer and use transferrable objects
        if (typeof this._options.style === 'function') {
          style = Stringify.functionToString(this._options.style);
        }
  
        var pointGeometry = this._options.pointGeometry;
  
        // TODO: Convert to buffer and use transferrable objects
        if (typeof this._options.pointGeometry === 'function') {
          pointGeometry = Stringify.functionToString(this._options.pointGeometry);
        }
  
        var geojson = _geojson;
        var transferrables = [];
  
        var layers = [];
  
        if (this._options.layers) {
          layers = this._options.layers;
        }
  
        // TODO: Allow filter method to be run here
        if (typeof geojson !== 'string') {
          // TODO: De-dupe with non-object processing in next section
          var fc = GeoJSON.collectFeatures(geojson, layers, this._options.topojson);
          var features = fc.features;
  
          // Run filter, if provided
          if (this._options.filter) {
            fc.features = features.filter(this._options.filter);
          }
  
          if (this._options.onEachFeature) {
            var feature;
            for (var i = 0; i < features.length; i++) {
              feature = features[i];
              this._options.onEachFeature(feature);
            };
          }
  
          this._geojson = geojson = Buffer.stringToUint8Array(JSON.stringify(fc));
          transferrables.push(geojson.buffer);
          this._execWorker(geojson, this._options.topojson, this._options.headers, this._world._originPoint, style, this._options.interactive, pointGeometry, transferrables).then(() => {
            resolve();
          }).catch(reject);
        } else if (typeof this._options.filter === 'function' || typeof this._options.onEachFeature === 'function') {
          GeoJSONWorkerLayer.RequestGeoJSON(geojson).then((res) => {
            // if (this._aborted) {
            //   resolve();
            //   return;
            // }
  
            var fc = GeoJSON.collectFeatures(res, layers, this._options.topojson);
            var features = fc.features;
  
            // Run filter, if provided
            if (this._options.filter) {
              fc.features = features.filter(this._options.filter);
            }
  
            if (this._options.onEachFeature) {
              var feature;
              for (var i = 0; i < features.length; i++) {
                feature = features[i];
                this._options.onEachFeature(feature);
              };
            }
  
            this._geojson = geojson = Buffer.stringToUint8Array(JSON.stringify(fc));
            transferrables.push(geojson.buffer);
  
            this._execWorker(geojson, false, this._options.headers, this._world._originPoint, style, this._options.interactive, pointGeometry, transferrables).then(() => {
              resolve();
            }).catch(reject);
          });
        } else {
          this._execWorker(geojson, this._options.topojson, this._options.headers, this._world._originPoint, style, this._options.interactive, pointGeometry, transferrables).then(() => {
            resolve();
          }).catch(reject);
        }
      });
    }
  
    _execWorker(geojson, topojson, headers, originPoint, style, interactive, pointGeometry, transferrables) {
      return new Promise((resolve, reject) => {
        // console.time('Worker round trip');
  
        Worker.exec('GeoJSONWorkerLayer.Process', [geojson, topojson, headers, originPoint, style, interactive, pointGeometry], transferrables).then((results) => {
          // console.timeEnd('Worker round trip');
  
          // if (this._aborted) {
          //   resolve();
          //   return;
          // }
  
          var processPromises = [];
  
          if (results.polygons) {
            processPromises.push(this._processPolygonResults(results.polygons));
          }
  
          if (results.polylines) {
            processPromises.push(this._processPolylineResults(results.polylines));
          }
  
          if (results.points) {
            processPromises.push(this._processPointResults(results.points));
          }
  
          if (processPromises.length > 0) {
            Promise.all(processPromises).then(() => {
              resolve();
            }).catch(reject);
          } else {
            resolve();
          }
        }).catch(reject);
      });
    }
  
    // TODO: Dedupe with polyline method
    _processPolygonResults(results) {
      return new Promise((resolve, reject) => {
        var splitPositions = Buffer.splitFloat32Array(results.attributes.positions);
        var splitNormals = Buffer.splitFloat32Array(results.attributes.normals);
        var splitColors = Buffer.splitFloat32Array(results.attributes.colors);
        var splitTops = Buffer.splitFloat32Array(results.attributes.tops);
  
        var splitOutlinePositions;
        var splitOutlineColors;
  
        if (results.outlineAttributes) {
          splitOutlinePositions = Buffer.splitFloat32Array(results.outlineAttributes.positions);
          splitOutlineColors = Buffer.splitFloat32Array(results.outlineAttributes.colors);
        }
  
        var splitProperties;
        if (results.properties) {
          splitProperties = Buffer.splitUint8Array(results.properties);
        }
  
        var flats = results.flats;
  
        var objects = [];
        var outlineObjects = [];
  
        var obj;
        var pickingId;
        var pickingIds;
        var properties;
  
        var polygonAttributeLengths = {
          positions: 3,
          normals: 3,
          colors: 3,
          tops: 1
        };
  
        var polygonOutlineAttributeLengths = {
          positions: 3,
          colors: 3
        };
  
        for (var i = 0; i < splitPositions.length; i++) {
          if (splitProperties && splitProperties[i]) {
            properties = JSON.parse(Buffer.uint8ArrayToString(splitProperties[i]));
          } else {
            properties = {};
          }
  
          // WORKERS: obj.attributes should actually an array of polygons for
          // the feature, though the current logic isn't aware of that
          obj = {
            attributes: [{
              positions: splitPositions[i],
              normals: splitNormals[i],
              colors: splitColors[i],
              tops: splitTops[i]
            }],
            properties: properties,
            flat: flats[i]
          };
  
          // WORKERS: If interactive, generate unique ID for each feature, create
          // the buffer attributes and set up event listeners
          if (this._options.interactive) {
            pickingId = this.getPickingId();
  
            pickingIds = new Float32Array(splitPositions[i].length / 3);
            Buffer.fillTypedArray(pickingIds, pickingId);
  
            obj.attributes[0].pickingIds = pickingIds;
  
            polygonAttributeLengths.pickingIds = 1;
  
            this._addPicking(pickingId, properties);
          }
  
          // TODO: Make this specific to polygon attributes
          if (typeof this._options.onAddAttributes === 'function') {
            var customAttributes = this._options.onAddAttributes(obj.attributes[0], properties);
            var customAttribute;
            for (var key in customAttributes) {
              customAttribute = customAttributes[key];
              obj.attributes[0][key] = customAttribute.value;
              polygonAttributeLengths[key] = customAttribute.length;
            }
          }
  
          objects.push(obj);
        }
  
        for (var i = 0; i < splitOutlinePositions.length; i++) {
          obj = {
            attributes: [{
              positions: splitOutlinePositions[i],
              colors: splitOutlineColors[i]
            }],
            flat: true
          };
  
          outlineObjects.push(obj);
        }
  
        var polygonAttributes = [];
        var polygonOutlineAttributes = [];
  
        var polygonFlat = true;
  
        for (var i = 0; i < objects.length; i++) {
          obj = objects[i];
  
          // TODO: Work out why obj.flat is rarely set to something other than
          // true or false. Potentially undefined.
          if (polygonFlat && obj.flat === false) {
            polygonFlat = false;
          }
  
          var bufferAttributes = Buffer.mergeAttributes(obj.attributes);
          polygonAttributes.push(bufferAttributes);
        };
  
        for (var i = 0; i < outlineObjects.length; i++) {
          obj = outlineObjects[i];
  
          var bufferAttributes = Buffer.mergeAttributes(obj.attributes);
          polygonOutlineAttributes.push(bufferAttributes);
        };
  
        var outputPromises = [];
  
        var style;
  
        if (polygonAttributes.length > 0) {
          var mergedPolygonAttributes = Buffer.mergeAttributes(polygonAttributes);
  
          // TODO: Make this work when style is a function per feature
          style = (typeof this._options.style === 'function') ? this._options.style(objects[0]) : this._options.style;
          style = extend({}, GeoJSON.defaultStyle, style);
  
          outputPromises.push(this._setPolygonMesh(mergedPolygonAttributes, polygonAttributeLengths, style, polygonFlat));
        }
  
        if (polygonOutlineAttributes.length > 0) {
          var mergedPolygonOutlineAttributes = Buffer.mergeAttributes(polygonOutlineAttributes);
  
          style = (typeof this._options.style === 'function') ? this._options.style(objects[0]) : this._options.style;
          style = extend({}, GeoJSON.defaultStyle, style);
  
          if (style.outlineRenderOrder !== undefined) {
            style.lineRenderOrder = style.outlineRenderOrder;
          } else {
            style.lineRenderOrder = (style.renderOrder) ? style.renderOrder + 1 : 4;
          }
  
          if (style.outlineWidth) {
            style.lineWidth = style.outlineWidth;
          }
  
          outputPromises.push(this._setPolylineMesh(mergedPolygonOutlineAttributes, polygonOutlineAttributeLengths, style, true));
        }
  
        Promise.all(outputPromises).then((results) => {
          var [polygonResult, outlineResult] = results;
  
          if (polygonResult) {
            this._polygonMesh = polygonResult.mesh;
            this.add(this._polygonMesh);
  
            if (polygonResult.pickingMesh) {
              this._pickingMesh.add(polygonResult.pickingMesh);
            }
          }
  
          if (outlineResult) {
            this.add(outlineResult.mesh);
          }
  
          resolve();
        }).catch(reject);
      });
    }
  
    // TODO: Dedupe with polygon method
    _processPolylineResults(results) {
      return new Promise((resolve, reject) => {
        var splitPositions = Buffer.splitFloat32Array(results.attributes.positions);
        var splitColors = Buffer.splitFloat32Array(results.attributes.colors);
  
        var splitProperties;
        if (results.properties) {
          splitProperties = Buffer.splitUint8Array(results.properties);
        }
  
        var flats = results.flats;
  
        var objects = [];
        var obj;
        var pickingId;
        var pickingIds;
        var properties;
  
        var polylineAttributeLengths = {
          positions: 3,
          colors: 3
        };
  
        for (var i = 0; i < splitPositions.length; i++) {
          if (splitProperties && splitProperties[i]) {
            properties = JSON.parse(Buffer.uint8ArrayToString(splitProperties[i]));
          } else {
            properties = {};
          }
  
          // WORKERS: obj.attributes should actually an array of polygons for
          // the feature, though the current logic isn't aware of that
          obj = {
            attributes: [{
              positions: splitPositions[i],
              colors: splitColors[i]
            }],
            properties: properties,
            flat: flats[i]
          };
  
          // WORKERS: If interactive, generate unique ID for each feature, create
          // the buffer attributes and set up event listeners
          if (this._options.interactive) {
            pickingId = this.getPickingId();
  
            pickingIds = new Float32Array(splitPositions[i].length / 3);
            Buffer.fillTypedArray(pickingIds, pickingId);
  
            obj.attributes[0].pickingIds = pickingIds;
  
            polylineAttributeLengths.pickingIds = 1;
  
            this._addPicking(pickingId, properties);
          }
  
          // TODO: Make this specific to polyline attributes
          if (typeof this._options.onAddAttributes === 'function') {
            var customAttributes = this._options.onAddAttributes(obj.attributes[0], properties);
            var customAttribute;
            for (var key in customAttributes) {
              customAttribute = customAttributes[key];
              obj.attributes[0][key] = customAttribute.value;
              polylineAttributeLengths[key] = customAttribute.length;
            }
          }
  
          objects.push(obj);
        }
  
        var polylineAttributes = [];
  
        var polylineFlat = true;
  
        for (var i = 0; i < objects.length; i++) {
          obj = objects[i];
  
          if (polylineFlat && !obj.flat) {
            polylineFlat = false;
          }
  
          var bufferAttributes = Buffer.mergeAttributes(obj.attributes);
          polylineAttributes.push(bufferAttributes);
        };
  
        if (polylineAttributes.length > 0) {
          var mergedPolylineAttributes = Buffer.mergeAttributes(polylineAttributes);
  
          // TODO: Make this work when style is a function per feature
          var style = (typeof this._options.style === 'function') ? this._options.style(objects[0]) : this._options.style;
          style = extend({}, GeoJSON.defaultStyle, style);
  
          this._setPolylineMesh(mergedPolylineAttributes, polylineAttributeLengths, style, polylineFlat).then((result) => {
            this._polylineMesh = result.mesh;
            this.add(this._polylineMesh);
  
            if (result.pickingMesh) {
              this._pickingMesh.add(result.pickingMesh);
            }
  
            resolve();
          }).catch(reject);
        } else {
          resolve();
        }
      });
    }
  
    _processPointResults(results) {
      return new Promise((resolve, reject) => {
        var splitPositions = Buffer.splitFloat32Array(results.attributes.positions);
        var splitNormals = Buffer.splitFloat32Array(results.attributes.normals);
        var splitColors = Buffer.splitFloat32Array(results.attributes.colors);
  
        var splitProperties;
        if (results.properties) {
          splitProperties = Buffer.splitUint8Array(results.properties);
        }
  
        var flats = results.flats;
  
        var objects = [];
        var obj;
        var pickingId;
        var pickingIds;
        var properties;
  
        var pointAttributeLengths = {
          positions: 3,
          normals: 3,
          colors: 3
        };
  
        for (var i = 0; i < splitPositions.length; i++) {
          if (splitProperties && splitProperties[i]) {
            properties = JSON.parse(Buffer.uint8ArrayToString(splitProperties[i]));
          } else {
            properties = {};
          }
  
          // WORKERS: obj.attributes should actually an array of polygons for
          // the feature, though the current logic isn't aware of that
          obj = {
            attributes: [{
              positions: splitPositions[i],
              normals: splitNormals[i],
              colors: splitColors[i]
            }],
            properties: properties,
            flat: flats[i]
          };
  
          // WORKERS: If interactive, generate unique ID for each feature, create
          // the buffer attributes and set up event listeners
          if (this._options.interactive) {
            pickingId = this.getPickingId();
  
            pickingIds = new Float32Array(splitPositions[i].length / 3);
            Buffer.fillTypedArray(pickingIds, pickingId);
  
            obj.attributes[0].pickingIds = pickingIds;
  
            pointAttributeLengths.pickingIds = 1;
  
            this._addPicking(pickingId, properties);
          }
  
          // TODO: Make this specific to polygon attributes
          if (typeof this._options.onAddAttributes === 'function') {
            var customAttributes = this._options.onAddAttributes(obj.attributes[0], properties);
            var customAttribute;
            for (var key in customAttributes) {
              customAttribute = customAttributes[key];
              obj.attributes[0][key] = customAttribute.value;
              pointAttributeLengths[key] = customAttribute.length;
            }
          }
  
          objects.push(obj);
        }
  
        var pointAttributes = [];
  
        var pointFlat = true;
  
        for (var i = 0; i < objects.length; i++) {
          obj = objects[i];
  
          if (pointFlat && !obj.flat) {
            pointFlat = false;
          }
  
          var bufferAttributes = Buffer.mergeAttributes(obj.attributes);
          pointAttributes.push(bufferAttributes);
        };
  
        if (pointAttributes.length > 0) {
          var mergedPointAttributes = Buffer.mergeAttributes(pointAttributes);
  
          // TODO: Make this work when style is a function per feature
          var style = (typeof this._options.style === 'function') ? this._options.style(objects[0]) : this._options.style;
          style = extend({}, GeoJSON.defaultStyle, style);
  
          this._setPointMesh(mergedPointAttributes, pointAttributeLengths, style, pointFlat).then((result) => {
            this._pointMesh = result.mesh;
            this.add(this._pointMesh);
  
            if (result.pickingMesh) {
              this._pickingMesh.add(result.pickingMesh);
            }
  
            resolve();
          }).catch(reject);
        } else {
          resolve();
        }
      });
    }
  
    // TODO: At some point this needs to return all the features to the main thread
    // so it can generate meshes and output to the scene, as well as perhaps creating
    // individual layers / components for each feature to track things like picking
    // and properties
    //
    // TODO: Find a way so the origin point isn't needed to be passed in as it
    // feels a bit messy and against the idea of a static Geo class
    //
    // TODO: Support passing custom geometry for point layers
    static Process(geojson, topojson, headers, originPoint, _style, _properties, _pointGeometry) {
      return new Promise((resolve, reject) => {
        GeoJSONWorkerLayer.ProcessGeoJSON(geojson, headers).then((res) => {
          var geojson = res;
  
          if (!geojson.features) {
            // Collects features into a single FeatureCollection
            //
            // Also converts TopoJSON to GeoJSON if instructed
            geojson = GeoJSON.collectFeatures(geojson, topojson);
          }
  
          // TODO: Check that GeoJSON is valid / usable
  
          var features = geojson.features;
  
          // TODO: Run filter, if provided (must be static)
  
          var pointScale;
          var polygons = [];
          var polylines = [];
          var points = [];
  
          // Deserialise style function if provided
          if (typeof _style === 'string') {
            _style = Stringify.stringToFunction(_style);
          }
  
          // Assume that a style won't be set per feature
          var style = _style;
  
          var pointGeometry;
          // Deserialise pointGeometry function if provided
          if (typeof _pointGeometry === 'string') {
            pointGeometry = Stringify.stringToFunction(_pointGeometry);
          }
  
          var feature;
          for (var i = 0; i < features.length; i++) {
            feature = features[i];
  
            var geometry = feature.geometry;
            var coordinates = (geometry.coordinates) ? geometry.coordinates : null;
  
            if (!coordinates || !geometry) {
              return;
            }
  
            // Get per-feature style object, if provided
            if (typeof _style === 'function') {
              style = extend({}, GeoJSON.defaultStyle, _style(feature));
              // console.log(feature, style);
            }
  
            if (geometry.type === 'Polygon' || geometry.type === 'MultiPolygon') {
              coordinates = (PolygonLayer.isSingle(coordinates)) ? [coordinates] : coordinates;
  
              var converted = coordinates.map(_coordinates => {
                return _coordinates.map(ring => {
                  return ring.map(coordinate => {
                    return LatLon(coordinate[1], coordinate[0]);
                  });
                });
              });
  
              var point;
              var projected = converted.map((_coordinates) => {
                return _coordinates.map((ring) => {
                  return ring.map((latlon) => {
                    point = Geo.latLonToPoint(latlon)._subtract(originPoint);
  
                    if (!pointScale) {
                      pointScale = Geo.pointScale(latlon);
                    }
  
                    return point;
                  });
                });
              });
  
              var polygon = {
                projected: projected,
                options: {
                  pointScale: pointScale,
                  style: style
                }
              };
  
              if (_properties) {
                polygon.properties = feature.properties;
              }
  
              polygons.push(polygon);
            }
  
            if (geometry.type === 'LineString' || geometry.type === 'MultiLineString') {
              coordinates = (PolylineLayer.isSingle(coordinates)) ? [coordinates] : coordinates;
  
              var converted = coordinates.map(_coordinates => {
                return _coordinates.map(coordinate => {
                  return LatLon(coordinate[1], coordinate[0]);
                });
              });
  
              var point;
              var projected = converted.map((_coordinates) => {
                return _coordinates.map((latlon) => {
                  point = Geo.latLonToPoint(latlon)._subtract(originPoint);
  
                  if (!pointScale) {
                    pointScale = Geo.pointScale(latlon);
                  }
  
                  return point;
                });
              });
  
              var polyline = {
                projected: projected,
                options: {
                  pointScale: pointScale,
                  style: style
                }
              };
  
              if (_properties) {
                polyline.properties = feature.properties;
              }
  
              polylines.push(polyline);
            }
  
            if (geometry.type === 'Point' || geometry.type === 'MultiPoint') {
              if (!pointGeometry) {
                console.warn('Skipping point geometry as no function provided');
                continue;
              }
  
              coordinates = (PointLayer.isSingle(coordinates)) ? [coordinates] : coordinates;
  
              var converted = coordinates.map(coordinate => {
                return LatLon(coordinate[1], coordinate[0]);
              });
  
              var point;
              var projected = converted.map((latlon) => {
                point = Geo.latLonToPoint(latlon)._subtract(originPoint);
  
                if (!pointScale) {
                  pointScale = Geo.pointScale(latlon);
                }
  
                return point;
              });
  
              var point = {
                projected: projected,
                options: {
                  pointGeometry: pointGeometry(feature),
                  pointScale: pointScale,
                  style: style
                }
              };
  
              if (_properties) {
                point.properties = feature.properties;
              }
  
              points.push(point);
            }
          };
  
          var polygonBufferPromises = [];
          var polylineBufferPromises = [];
          var pointBufferPromises = [];
  
          var polygon;
          for (var i = 0; i < polygons.length; i++) {
            polygon = polygons[i];
            polygonBufferPromises.push(PolygonLayer.SetBufferAttributes(polygon.projected, polygon.options));
          };
  
          var polyline;
          for (var i = 0; i < polylines.length; i++) {
            polyline = polylines[i];
            polylineBufferPromises.push(PolylineLayer.SetBufferAttributes(polyline.projected, polyline.options));
          };
  
          var point;
          for (var i = 0; i < points.length; i++) {
            point = points[i];
            pointBufferPromises.push(PointLayer.SetBufferAttributes(point.projected, point.options));
          };
  
          var data = {};
          var transferrables = [];
  
          // TODO: Make this work with polylines too
          // TODO: Make this so it's not a nest of promises
          GeoJSONWorkerLayer.ProcessPolygons(polygonBufferPromises, polygons, _properties).then((result) => {
            data.polygons = result.data;
            transferrables = transferrables.concat(result.transferrables);
  
            GeoJSONWorkerLayer.ProcessPolylines(polylineBufferPromises, polylines, _properties).then((result) => {
              data.polylines = result.data;
              transferrables = transferrables.concat(result.transferrables);
  
              GeoJSONWorkerLayer.ProcessPoints(pointBufferPromises, points, _properties).then((result) => {
                data.points = result.data;
                transferrables = transferrables.concat(result.transferrables);
  
                resolve({
                  data: data,
                  transferrables: transferrables
                });
              });
            });
          });
        }).catch(reject);
      });
    }
  
    static ProcessPolygons(polygonPromises, polygons, _properties) {
      return new Promise((resolve, reject) => {
        Promise.all(polygonPromises).then((results) => {
          var transferrables = [];
  
          var positions = [];
          var normals = [];
          var colors = [];
          var tops = [];
  
          var outlinePositions = [];
          var outlineColors = [];
  
          var properties = [];
  
          var flats = [];
          var polygon;
  
          var result;
          for (var i = 0; i < results.length; i++) {
            result = results[i];
  
            polygon = polygons[i];
  
            // WORKERS: Making this a typed array will speed up transfer time
            // As things stand this adds on a few milliseconds
            flats.push(result.flat);
  
            // WORKERS: result.attributes is actually an array of polygons for each
            // feature, though the current logic isn't keeping these all together
  
            var attributes;
            for (var j = 0; j < result.attributes.length; j++) {
              attributes = result.attributes[j];
  
              positions.push(attributes.positions);
              normals.push(attributes.normals);
              colors.push(attributes.colors);
              tops.push(attributes.tops);
  
              if (_properties) {
                properties.push(Buffer.stringToUint8Array(JSON.stringify(polygon.properties)));
              }
            };
  
            var outlineAttributes;
            for (var j = 0; j < result.outlineAttributes.length; j++) {
              outlineAttributes = result.outlineAttributes[j];
  
              outlinePositions.push(outlineAttributes.positions);
              outlineColors.push(outlineAttributes.colors);
            };
          };
  
          var mergedAttributes = {
            positions: Buffer.mergeFloat32Arrays(positions),
            normals: Buffer.mergeFloat32Arrays(normals),
            colors: Buffer.mergeFloat32Arrays(colors),
            tops: Buffer.mergeFloat32Arrays(tops)
          };
  
          var mergedOutlineAttributes = {
            positions: Buffer.mergeFloat32Arrays(outlinePositions),
            colors: Buffer.mergeFloat32Arrays(outlineColors)
          };
  
          transferrables.push(mergedAttributes.positions[0].buffer);
          transferrables.push(mergedAttributes.positions[1].buffer);
  
          transferrables.push(mergedAttributes.normals[0].buffer);
          transferrables.push(mergedAttributes.normals[1].buffer);
  
          transferrables.push(mergedAttributes.colors[0].buffer);
          transferrables.push(mergedAttributes.colors[1].buffer);
  
          transferrables.push(mergedAttributes.tops[0].buffer);
          transferrables.push(mergedAttributes.tops[1].buffer);
  
          transferrables.push(mergedOutlineAttributes.positions[0].buffer);
          transferrables.push(mergedOutlineAttributes.positions[1].buffer);
  
          transferrables.push(mergedOutlineAttributes.colors[0].buffer);
          transferrables.push(mergedOutlineAttributes.colors[1].buffer);
  
          var mergedProperties;
          if (_properties) {
            mergedProperties = Buffer.mergeUint8Arrays(properties);
  
            transferrables.push(mergedProperties[0].buffer);
            transferrables.push(mergedProperties[1].buffer);
          }
  
          var output = {
            attributes: mergedAttributes,
            outlineAttributes: mergedOutlineAttributes,
            flats: flats
          };
  
          if (_properties) {
            output.properties = mergedProperties;
          }
  
          // TODO: Also return GeoJSON features that can be mapped to objects on
          // the main thread. Allow user to provide filter / toggles to only return
          // properties from the GeoJSON that they need (eg. don't return geometry,
          // or don't return properties.height)
          resolve({
            data: output,
            transferrables: transferrables
          });
        }).catch(reject);
      });
    }
  
    static ProcessPolylines(polylinePromises, polylines, _properties) {
      return new Promise((resolve, reject) => {
        Promise.all(polylinePromises).then((results) => {
          var transferrables = [];
  
          var positions = [];
          var colors = [];
  
          var properties = [];
  
          var flats = [];
          var polyline;
  
          var result;
          for (var i = 0; i < results.length; i++) {
            result = results[i];
  
            polyline = polylines[i];
  
            // WORKERS: Making this a typed array will speed up transfer time
            // As things stand this adds on a few milliseconds
            flats.push(result.flat);
  
            // WORKERS: result.attributes is actually an array of polygons for each
            // feature, though the current logic isn't keeping these all together
  
            var attributes;
            for (var j = 0; j < result.attributes.length; j++) {
              attributes = result.attributes[j];
  
              positions.push(attributes.positions);
              colors.push(attributes.colors);
  
              if (_properties) {
                properties.push(Buffer.stringToUint8Array(JSON.stringify(polyline.properties)));
              }
            };
          };
  
          var mergedAttributes = {
            positions: Buffer.mergeFloat32Arrays(positions),
            colors: Buffer.mergeFloat32Arrays(colors)
          };
  
          transferrables.push(mergedAttributes.positions[0].buffer);
          transferrables.push(mergedAttributes.positions[1].buffer);
  
          transferrables.push(mergedAttributes.colors[0].buffer);
          transferrables.push(mergedAttributes.colors[1].buffer);
  
          var mergedProperties;
          if (_properties) {
            mergedProperties = Buffer.mergeUint8Arrays(properties);
  
            transferrables.push(mergedProperties[0].buffer);
            transferrables.push(mergedProperties[1].buffer);
          }
  
          var output = {
            attributes: mergedAttributes,
            flats: flats
          };
  
          if (_properties) {
            output.properties = mergedProperties;
          }
  
          // TODO: Also return GeoJSON features that can be mapped to objects on
          // the main thread. Allow user to provide filter / toggles to only return
          // properties from the GeoJSON that they need (eg. don't return geometry,
          // or don't return properties.height)
          resolve({
            data: output,
            transferrables: transferrables
          });
        }).catch(reject);
      });
    }
  
    // TODO: Dedupe with ProcessPolygons as they are identical
    static ProcessPoints(pointPromises, points, _properties) {
      return new Promise((resolve, reject) => {
        Promise.all(pointPromises).then((results) => {
          var transferrables = [];
  
          var positions = [];
          var normals = [];
          var colors = [];
  
          var properties = [];
  
          var flats = [];
          var point;
  
          var result;
          for (var i = 0; i < results.length; i++) {
            result = results[i];
  
            point = points[i];
  
            // WORKERS: Making this a typed array will speed up transfer time
            // As things stand this adds on a few milliseconds
            flats.push(result.flat);
  
            // WORKERS: result.attributes is actually an array of polygons for each
            // feature, though the current logic isn't keeping these all together
  
            var attributes;
            for (var j = 0; j < result.attributes.length; j++) {
              attributes = result.attributes[j];
  
              positions.push(attributes.positions);
              normals.push(attributes.normals);
              colors.push(attributes.colors);
  
              if (_properties) {
                properties.push(Buffer.stringToUint8Array(JSON.stringify(polygon.properties)));
              }
            };
          };
  
          var mergedAttributes = {
            positions: Buffer.mergeFloat32Arrays(positions),
            normals: Buffer.mergeFloat32Arrays(normals),
            colors: Buffer.mergeFloat32Arrays(colors)
          };
  
          transferrables.push(mergedAttributes.positions[0].buffer);
          transferrables.push(mergedAttributes.positions[1].buffer);
  
          transferrables.push(mergedAttributes.normals[0].buffer);
          transferrables.push(mergedAttributes.normals[1].buffer);
  
          transferrables.push(mergedAttributes.colors[0].buffer);
          transferrables.push(mergedAttributes.colors[1].buffer);
  
          var mergedProperties;
          if (_properties) {
            mergedProperties = Buffer.mergeUint8Arrays(properties);
  
            transferrables.push(mergedProperties[0].buffer);
            transferrables.push(mergedProperties[1].buffer);
          }
  
          var output = {
            attributes: mergedAttributes,
            flats: flats
          };
  
          if (_properties) {
            output.properties = mergedProperties;
          }
  
          // TODO: Also return GeoJSON features that can be mapped to objects on
          // the main thread. Allow user to provide filter / toggles to only return
          // properties from the GeoJSON that they need (eg. don't return geometry,
          // or don't return properties.height)
          resolve({
            data: output,
            transferrables: transferrables
          });
        }).catch(reject);
      });
    }
  
    static ProcessGeoJSON(geojson, headers) {
      if (typeof geojson === 'string') {
        return GeoJSONWorkerLayer.RequestGeoJSON(geojson, headers);
      } else {
        return Promise.resolve(JSON.parse(Buffer.uint8ArrayToString(geojson)));
      }
    }
  
    static RequestGeoJSON(path, headers) {
      return reqwest({
        url: path,
        type: 'json',
        crossOrigin: true,
        headers: headers
      });
    }
  
    // Create and store mesh from buffer attributes
    //
    // Could make this an abstract method for each geometry layer
    _setPolygonMesh(attributes, attributeLengths, style, flat) {
      if (!this._world) {
        return Promise.reject();
      }
  
      return PolygonLayer.SetMesh(attributes, attributeLengths, flat, style, this._options, this._world._environment._skybox);
    }
  
    _setPolylineMesh(attributes, attributeLengths, style, flat) {
      if (!this._world) {
        return Promise.reject();
      }
  
      return PolylineLayer.SetMesh(attributes, attributeLengths, flat, style, this._options);
    }
  
    _setPointMesh(attributes, attributeLengths, style, flat) {
      if (!this._world) {
        return Promise.reject();
      }
  
      return PointLayer.SetMesh(attributes, attributeLengths, flat, style, this._options, this._world._environment._skybox);
    }
  
    // Set up and re-emit interaction events
    _addPicking(pickingId, properties) {
      this._world.on('pick-click-' + pickingId, (pickingId, point2d, point3d, intersects) => {
        this._world.emit('click', this, properties, point2d, point3d);
      });
  
      this._world.on('pick-hover-' + pickingId, (pickingId, point2d, point3d, intersects) => {
        this._world.emit('hover', this, properties, point2d, point3d);
      });
    }
  
    // TODO: Finish cleanup
    destroy() {
      // Run common destruction logic from parent
      super.destroy();
    }
  }
  
  export default GeoJSONWorkerLayer;
  
  var noNew = function(geojson, options) {
    return new GeoJSONWorkerLayer(geojson, options);
  };
  
  export {noNew as geoJSONWorkerLayer};
  