text: 'use-strict'
  
  // base libs
  import PropTypes from 'prop-types'
  import React, { PureComponent } from 'react'
  import {
    Platform,
    Dimensions,
    LayoutAnimation
  } from 'react-native'
  // map-related libs
  import MapView from 'react-native-maps'
  import SuperCluster from 'supercluster'
  import GeoViewport from '@mapbox/geo-viewport'
  // components / views
  import ClusterMarker from './ClusterMarker'
  // libs / utils
  import {
    regionToBoundingBox,
    itemToGeoJSONFeature,
    ensureLongitude,
    deg2rad
  } from './util'
  
  export default class ClusteredMapView extends PureComponent {
  
    constructor(props) {
      super(props)
  
      this.state = {
        data: [], // helds renderable clusters and markers
        region: props.region || props.initialRegion, // helds current map region
      }
  
      this.isAndroid = Platform.OS === 'android'
      this.dimensions = [props.width, props.height]
  
      this.mapRef = this.mapRef.bind(this)
      this.onClusterPress = this.onClusterPress.bind(this)
      this.onRegionChangeComplete = this.onRegionChangeComplete.bind(this)
    }
  
    componentDidMount() {
      this.clusterize(this.props.data)
    }
  
    componentWillReceiveProps(nextProps) {
      if (this.props.data !== nextProps.data)
        this.clusterize(nextProps.data)
    }
  
    componentWillUpdate(nextProps, nextState) {
      if (!this.isAndroid && this.props.animateClusters && this.clustersChanged(nextState))
        LayoutAnimation.configureNext(this.props.layoutAnimationConf)
    }
  
    mapRef(ref) {
      this.mapview = ref
    }
  
    getMapRef() {
      return this.mapview
    }
  
    getClusteringEngine() {
      return this.index
    }
    
    clustersComparator = ({
        geometry: { coordinates: c1 },
        properties: { point_count: pc1 }
    }) => ({
        geometry: { coordinates: c2 },
        properties: { point_count: pc2 }
    }) => {
        return (
            pc1 === pc2 &&
            Math.abs(c1[0] - c2[0]) < Number.EPSILON &&
            Math.abs(c1[1] - c2[1]) < Number.EPSILON
        );
    };
  
    preserveUnchangedClusterIds(data) {
      const prevClusters = this.state.data.filter(d => d.properties.cluster);
      data.filter(d => d.properties.cluster).forEach(cluster => {
          const comparator = this.clustersComparator(cluster);
          const item = prevClusters.find(comparator);
          if (item) {
              cluster.properties.cluster_id = item.properties.cluster_id;
          }
      });
      return data;
  }
  
    clusterize(dataset) {
      this.index = new SuperCluster({ // eslint-disable-line new-cap
        extent: this.props.extent,
        minZoom: this.props.minZoom,
        maxZoom: this.props.maxZoom,
        radius: this.props.radius || (this.dimensions[0] * .045), // 4.5% of screen width
      })
  
      // get formatted GeoPoints for cluster
      const rawData = dataset.map(item => itemToGeoJSONFeature(item, this.props.accessor))
  
      // load geopoints into SuperCluster
      this.index.load(rawData)
  
      const data = this.preserveUnchangedClusterIds(this.getClusters(this.state.region));
      this.setState({ data })
    }
  
    clustersChanged(nextState) {
      return this.state.data.length !== nextState.data.length
    }
  
    onRegionChangeComplete(region) {
      let data = this.preserveUnchangedClusterIds(this.getClusters(region));
      this.setState({ region, data }, () => {
          this.props.onRegionChangeComplete && this.props.onRegionChangeComplete(region, data)
      })
    }
  
    getClusters(region) {
      const bbox = regionToBoundingBox(region),
            viewport = (region.longitudeDelta) >= 40 ? { zoom: this.props.minZoom } : GeoViewport.viewport(bbox, this.dimensions)
  
      if (this.props.kmBboxWiden) {
        const latExtend = this.props.kmBboxWiden / 110.574;
        const lonExtend = 111.32 * Math.cos(deg2rad(region.latitude));
        bbox[0] = ensureLongitude(bbox[0] - lonExtend);
        bbox[1] -= latExtend;
        bbox[2] += ensureLongitude(bbox[0] + lonExtend);
        bbox[3] += latExtend;
      }
  
      return this.index.getClusters(bbox, viewport.zoom)
    }
  
    onClusterPress(cluster) {
  
      // cluster press behavior might be extremely custom.
      if (!this.props.preserveClusterPressBehavior) {
        this.props.onClusterPress && this.props.onClusterPress(cluster.properties.cluster_id)
        return
      }
  
      // //////////////////////////////////////////////////////////////////////////////////
      // NEW IMPLEMENTATION (with fitToCoordinates)
      // //////////////////////////////////////////////////////////////////////////////////
      // get cluster children
      const children = this.index.getLeaves(cluster.properties.cluster_id, this.props.clusterPressMaxChildren),
            markers = children.map(c => c.properties.item)
  
      // fit right around them, considering edge padding
      this.mapview.fitToCoordinates(markers.map(m => m[this.props.accessor]), { edgePadding: this.props.edgePadding })
  
      this.props.onClusterPress && this.props.onClusterPress(cluster.properties.cluster_id, markers)
    }
  
    render() {
      const { style, MapComponent, ...props } = this.props
  
      return (
        <MapComponent
          {...props}
          style={style}
          ref={this.mapRef}
          onRegionChangeComplete={this.onRegionChangeComplete}>
          {
            this.props.clusteringEnabled && this.state.data.map((d) => {
              if (d.properties.point_count === 0)
                return this.props.renderMarker(d.properties.item)
  
              return (
                <ClusterMarker
                  {...d}
                  onPress={this.onClusterPress}
                  renderCluster={this.props.renderCluster}
                  key={`cluster-${d.properties.cluster_id}`} />
              )
            })
          }
          {
            !this.props.clusteringEnabled && this.props.data.map(d => this.props.renderMarker(d))
          }
          {this.props.children}
        </MapComponent>
      )
    }
  }
  
  ClusteredMapView.defaultProps = {
    minZoom: 1,
    maxZoom: 16,
    extent: 512,
    kmBboxWiden: 0,
    accessor: 'location',
    animateClusters: true,
    clusteringEnabled: true,
    clusterPressMaxChildren: 100,
    preserveClusterPressBehavior: true,
    width: Dimensions.get('window').width,
    height: Dimensions.get('window').height,
    layoutAnimationConf: LayoutAnimation.Presets.spring,
    edgePadding: { top: 10, left: 10, right: 10, bottom: 10 }
  }
  
  ClusteredMapView.propTypes = {
    ...MapView.propTypes,
    // number
    radius: PropTypes.number,
    width: PropTypes.number.isRequired,
    height: PropTypes.number.isRequired,
    extent: PropTypes.number.isRequired,
    minZoom: PropTypes.number.isRequired,
    maxZoom: PropTypes.number.isRequired,
    clusterPressMaxChildren: PropTypes.number.isRequired,
    kmBboxWiden: PropTypes.number.isRequired,
    // array
    data: PropTypes.array.isRequired,
    // func
    onExplode: PropTypes.func,
    onImplode: PropTypes.func,
    onClusterPress: PropTypes.func,
    renderMarker: PropTypes.func.isRequired,
    renderCluster: PropTypes.func.isRequired,
    // bool
    animateClusters: PropTypes.bool.isRequired,
    clusteringEnabled: PropTypes.bool.isRequired,
    preserveClusterPressBehavior: PropTypes.bool.isRequired,
    // object
    layoutAnimationConf: PropTypes.object,
    edgePadding: PropTypes.object.isRequired,
    // string
    // mutiple
    accessor: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
    MapComponent: PropTypes.node.isRequired
  }
  