[
  {
    "path": ".eslintrc",
    "content": "\n{\n  \"extends\": \"airbnb\",\n  \"parser\": \"babel-eslint\",\n  \"rules\": {\n    \"no-nested-ternary\": 0,\n    \"react/prop-types\": 0\n  }\n}\n"
  },
  {
    "path": ".gitignore",
    "content": "/build/*\n!/build/index.html\n/node_modules\nnpm-debug.log\n"
  },
  {
    "path": "README.md",
    "content": "\n# google-map-clustering-example\n\nClustering example for [google-map-react](https://github.com/istarkov/google-map-react)\n\n[Click here to view](http://istarkov.github.io/google-map-clustering-example/)\n\n### [Kotatsu](https://github.com/Yomguithereal/kotatsu)\n\nI was heavily inspired of [kotatsu](https://github.com/Yomguithereal/kotatsu) project. \n\nThis project uses `kotatsu` as run engine.\n\nTo run this project locally (with hot reload and other fine kotatsu things)\n\n```bash\nnpm install\nnpm run start\n# open your browser at localhost:4000\n```\n\nTo build\n\n```bash\nnpm install\nnpm run build\n```\n\n### [MapBox](https://github.com/mapbox) and [Mourner](https://github.com/mourner)\n\nAny map-geo library you want, always can be found there.\n\nThis project also uses [MapBox library written by Mourner](https://github.com/mapbox/supercluster)\n\n\n### [Recompose by @acdlite](https://github.com/acdlite/recompose)\n\nMy lovest library, [recompose](https://github.com/acdlite/recompose) is heavily used in this project.\n\n\n\n"
  },
  {
    "path": "build/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Map clustering example</title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"text/javascript\" src=\"./bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "config/loaders.js",
    "content": "var path = require('path'); // eslint-disable-line no-var\nvar autoprefixer = require('autoprefixer');  // eslint-disable-line no-var\n\nmodule.exports = {\n  postcss: [\n    autoprefixer({ browsers: ['last 2 versions'] }),\n  ],\n  module: {\n    loaders: [\n      {\n        test: /\\.sass$/,\n        loaders: [\n          'style-loader',\n          'css-loader?modules&importLoaders=2&localIdentName=[name]__[local]',\n          'postcss-loader',\n          `sass-loader?precision=10&indentedSyntax=sass`,\n        ],\n        include: [\n          path.join(__dirname, '../src'),\n        ],\n      },\n      {\n        test: /\\.css$/,\n        loaders: [\n          'style-loader',\n          'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]',\n          'postcss-loader',\n        ],\n        include: [\n          path.join(__dirname, '../src'),\n          path.join(__dirname, '../node_modules'),\n        ],\n      },\n      {\n        test: /\\.svg$/,\n        loaders: ['url-loader?limit=7000'],\n      },\n    ],\n  },\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"google-map-clustering-example\",\n  \"version\": \"0.1.0\",\n  \"description\": \"clustering example for google-map-react\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"kotatsu serve --port 4000 --config ./config/loaders.js --presets es2015,stage-0,react,react-hmre ./src/Main.js\",\n    \"build\": \"NODE_ENV=production kotatsu build client --minify --config ./config/loaders.js --presets es2015,stage-0,react ./src/Main.js -o build\",\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"author\": \"Ivan Starkov\",\n  \"license\": \"MIT\",\n  \"devDependencies\": {\n    \"autoprefixer\": \"^6.3.1\",\n    \"babel-cli\": \"^6.4.5\",\n    \"babel-eslint\": \"^4.1.8\",\n    \"babel-preset-es2015\": \"^6.3.13\",\n    \"babel-preset-react\": \"^6.3.13\",\n    \"babel-preset-react-hmre\": \"^1.0.1\",\n    \"babel-preset-stage-0\": \"^6.3.13\",\n    \"css-loader\": \"^0.23.1\",\n    \"eslint\": \"^1.10.3\",\n    \"eslint-config-airbnb\": \"^4.0.0\",\n    \"eslint-plugin-react\": \"^3.16.1\",\n    \"file-loader\": \"^0.8.5\",\n    \"kotatsu\": \"^0.9.1\",\n    \"node-sass\": \"^3.4.2\",\n    \"normalize.css\": \"^3.0.3\",\n    \"postcss-loader\": \"^0.8.0\",\n    \"sass-loader\": \"^3.1.2\",\n    \"style-loader\": \"^0.13.0\",\n    \"url-loader\": \"^0.5.7\"\n  },\n  \"dependencies\": {\n    \"google-map-react\": \"^0.14.5\",\n    \"points-cluster\": \"^0.1.4\",\n    \"react\": \"^15.1.0\",\n    \"react-dom\": \"^15.1.0\",\n    \"react-motion\": \"^0.4.4\",\n    \"recompose\": \"^0.20.0\"\n  }\n}\n"
  },
  {
    "path": "src/GMap.js",
    "content": "import React from 'react';\nimport compose from 'recompose/compose';\nimport defaultProps from 'recompose/defaultProps';\nimport withState from 'recompose/withState';\nimport withHandlers from 'recompose/withHandlers';\nimport withPropsOnChange from 'recompose/withPropsOnChange';\nimport GoogleMapReact from 'google-map-react';\nimport ClusterMarker from './markers/ClusterMarker';\nimport SimpleMarker from './markers/SimpleMarker';\nimport supercluster from 'points-cluster';\nimport { susolvkaCoords, markersData } from './data/fakeData';\n\nexport const gMap = ({\n  style, hoverDistance, options,\n  mapProps: { center, zoom },\n  onChange, onChildMouseEnter, onChildMouseLeave,\n  clusters,\n}) => (\n  <GoogleMapReact\n    style={style}\n    options={options}\n    hoverDistance={hoverDistance}\n    center={center}\n    zoom={zoom}\n    onChange={onChange}\n    onChildMouseEnter={onChildMouseEnter}\n    onChildMouseLeave={onChildMouseLeave}\n  >\n    {\n      clusters\n        .map(({ ...markerProps, id, numPoints }) => (\n          numPoints === 1\n            ? <SimpleMarker key={id} {...markerProps} />\n            : <ClusterMarker key={id} {...markerProps} />\n        ))\n    }\n  </GoogleMapReact>\n);\n\nexport const gMapHOC = compose(\n  defaultProps({\n    clusterRadius: 60,\n    hoverDistance: 30,\n    options: {\n      minZoom: 3,\n      maxZoom: 15,\n    },\n    style: {\n      position: 'relative',\n      margin: 0,\n      padding: 0,\n      flex: 1,\n    },\n  }),\n  // withState so you could change markers if you want\n  withState(\n    'markers',\n    'setMarkers',\n    markersData\n  ),\n  withState(\n    'hoveredMarkerId',\n    'setHoveredMarkerId',\n    -1\n  ),\n  withState(\n    'mapProps',\n    'setMapProps',\n    {\n      center: susolvkaCoords,\n      zoom: 10,\n    }\n  ),\n  // describe events\n  withHandlers({\n    onChange: ({ setMapProps }) => ({ center, zoom, bounds }) => {\n      setMapProps({ center, zoom, bounds });\n    },\n\n    onChildMouseEnter: ({ setHoveredMarkerId }) => (hoverKey, { id }) => {\n      setHoveredMarkerId(id);\n    },\n\n    onChildMouseLeave: ({ setHoveredMarkerId }) => (/* hoverKey, childProps */) => {\n      setHoveredMarkerId(-1);\n    },\n  }),\n  // precalculate clusters if markers data has changed\n  withPropsOnChange(\n    ['markers'],\n    ({ markers = [], clusterRadius, options: { minZoom, maxZoom } }) => ({\n      getCluster: supercluster(\n        markers,\n        {\n          minZoom, // min zoom to generate clusters on\n          maxZoom, // max zoom level to cluster the points on\n          radius: clusterRadius, // cluster radius in pixels\n        }\n      ),\n    })\n  ),\n  // get clusters specific for current bounds and zoom\n  withPropsOnChange(\n    ['mapProps', 'getCluster'],\n    ({ mapProps, getCluster }) => ({\n      clusters: mapProps.bounds\n        ? getCluster(mapProps)\n          .map(({ wx, wy, numPoints, points }) => ({\n            lat: wy,\n            lng: wx,\n            text: numPoints,\n            numPoints,\n            id: `${numPoints}_${points[0].id}`,\n          }))\n        : [],\n    })\n  ),\n  // set hovered\n  withPropsOnChange(\n    ['clusters', 'hoveredMarkerId'],\n    ({ clusters, hoveredMarkerId }) => ({\n      clusters: clusters\n        .map(({ ...cluster, id }) => ({\n          ...cluster,\n          hovered: id === hoveredMarkerId,\n        })),\n    })\n  ),\n);\n\nexport default gMapHOC(gMap);\n"
  },
  {
    "path": "src/Layout.js",
    "content": "import React, { Component } from 'react';\nimport compose from 'recompose/compose';\nimport defaultProps from 'recompose/defaultProps';\nimport layoutStyles from './Layout.sass';\nimport GMap from './GMap';\n// for hmr to work I need the first class to extend Component\nexport class Layout extends Component {\n  render() {\n    const { styles: { layout, header, main, footer, logo } } = this.props;\n    return (\n      <div className={layout}>\n        <header className={header}>\n          <div>\n            Clustering example google-map-react (zoom, move to play with)\n          </div>\n          <div>\n            <a href=\"https://github.com/istarkov/google-map-clustering-example\">\n              Star at github.com\n            </a>\n          </div>\n        </header>\n        <main className={main}>\n          <GMap />\n        </main>\n        <footer className={footer}>\n          <div>\n            <a href=\"https://github.com/istarkov\">\n              Ivan Starkov\n            </a>\n          </div>\n          <div className={logo}></div>\n          <div>\n            <a href=\"https://twitter.com/icelabaratory\">\n              @icelabaratory\n            </a>\n          </div>\n        </footer>\n      </div>\n    );\n  }\n}\n\nexport const layoutHOC = compose(\n  defaultProps({\n    styles: layoutStyles,\n  })\n);\n\nexport default layoutHOC(Layout);\n"
  },
  {
    "path": "src/Layout.sass",
    "content": ".layout\n  display: flex\n  min-height: 100vh\n  flex-direction: column\n  margin: 0 1px 0 1px\n\n.header\n  height: 2em\n  background-color: #004336\n  color: #fff\n  display: flex\n  align-items: center\n  justify-content: space-between\n  padding: 0 10px 0 10px\n  a\n    color: #fff\n\n.logo\n  width: 1.3em\n  height: 1.3em\n  margin: 0.3em\n  background-size: contain\n  background-repeat: no-repeat\n  background-image: url('https://avatars2.githubusercontent.com/u/5077042?v=3&s=40')\n\n.main\n  flex: 1\n  display: flex\n\n.footer\n  height: 2em\n  background-color: #004336\n  color: #fff\n  display: flex\n  align-items: center\n  justify-content: center\n  a\n    color: #fff\n"
  },
  {
    "path": "src/Main.js",
    "content": "// file: main.jsx\nimport React from 'react';\nimport { render } from 'react-dom';\nimport Layout from './Layout.js';\n\nimport 'normalize.css/normalize.css';\nimport './Main.sass';\n\nconst mountNode = document.getElementById('app');\n\nrender(<Layout />, mountNode);\n"
  },
  {
    "path": "src/Main.sass",
    "content": ":global(#app)\n  height: 100%\n\nhtml, body\n  height: 100%\n  font-size: 14px\n"
  },
  {
    "path": "src/data/fakeData.js",
    "content": "\nconst TOTAL_COUNT = 200;\n\nexport const susolvkaCoords = { lat: 60.814305, lng: 47.051773 };\n\nexport const markersData = [...Array(TOTAL_COUNT)].fill(0) // fill(0) for loose mode\n  .map((__, index) => ({\n    id: index,\n    lat: susolvkaCoords.lat +\n      0.01 * index *\n      Math.sin(30 * Math.PI * index / 180) *\n      Math.cos(50 * Math.PI * index / 180) + Math.sin(5 * index / 180),\n    lng: susolvkaCoords.lng +\n      0.01 * index *\n      Math.cos(70 + 23 * Math.PI * index / 180) *\n      Math.cos(50 * Math.PI * index / 180) + Math.sin(5 * index / 180),\n  }));\n"
  },
  {
    "path": "src/markers/ClusterMarker.js",
    "content": "import React from 'react';\nimport compose from 'recompose/compose';\nimport defaultProps from 'recompose/defaultProps';\nimport withPropsOnChange from 'recompose/withPropsOnChange';\nimport pure from 'recompose/pure';\nimport { Motion, spring } from 'react-motion';\nimport clusterMarkerStyles from './ClusterMarker.sass';\n\nexport const clusterMarker = ({\n  styles, text,\n  defaultMotionStyle, motionStyle,\n}) => (\n  <Motion\n    defaultStyle={defaultMotionStyle}\n    style={motionStyle}\n  >\n  {\n    ({ scale }) => (\n      <div\n        className={styles.marker}\n        style={{\n          transform: `translate3D(0,0,0) scale(${scale}, ${scale})`,\n        }}\n      >\n        <div\n          className={styles.text}\n        >\n          {text}\n        </div>\n      </div>\n    )\n  }\n  </Motion>\n);\n\nexport const clusterMarkerHOC = compose(\n  defaultProps({\n    text: '0',\n    styles: clusterMarkerStyles,\n    initialScale: 0.6,\n    defaultScale: 1,\n    hoveredScale: 1.15,\n    hovered: false,\n    stiffness: 320,\n    damping: 7,\n    precision: 0.001,\n  }),\n  // pure optimization can cause some effects you don't want,\n  // don't use it in development for markers\n  pure,\n  withPropsOnChange(\n    ['initialScale'],\n    ({ initialScale, defaultScale, $prerender }) => ({\n      initialScale,\n      defaultMotionStyle: { scale: $prerender ? defaultScale : initialScale },\n    })\n  ),\n  withPropsOnChange(\n    ['hovered'],\n    ({\n      hovered, hoveredScale, defaultScale,\n      stiffness, damping, precision,\n    }) => ({\n      hovered,\n      motionStyle: {\n        scale: spring(\n          hovered ? hoveredScale : defaultScale,\n          { stiffness, damping, precision }\n        ),\n      },\n    })\n  )\n);\n\nexport default clusterMarkerHOC(clusterMarker);\n"
  },
  {
    "path": "src/markers/ClusterMarker.sass",
    "content": "@function stripUnits($number)\n  @return $number / ($number * 0 + 1)\n\n$marker-width: 40px !default\n$marker-height: 40px !default\n$marker-border-width: 5px !default\n$marker-font-size: 14px !default\n\n\n.marker\n  position: absolute\n  cursor: pointer\n  width: $marker-width\n  height: $marker-height\n  left: -$marker-width / 2\n  top: -$marker-height / 2\n\n  border: $marker-border-width solid #004336\n  border-radius: 50%\n  background-color: white\n  text-align: center\n  color: #333\n\n  font-size: $marker-font-size\n  font-weight: bold\n  display: flex\n  align-items: center\n  justify-content: center\n\n.text\n\n:export\n  markerSize: stripUnits($marker-width)\n"
  },
  {
    "path": "src/markers/SimpleMarker.js",
    "content": "import React from 'react';\nimport compose from 'recompose/compose';\nimport defaultProps from 'recompose/defaultProps';\n// import mapPropsOnChange from 'recompose/mapPropsOnChange';\nimport { Motion } from 'react-motion';\nimport { clusterMarkerHOC } from './ClusterMarker.js';\nimport simpleMarkerStyles from './SimpleMarker.sass';\n\nexport const simpleMarker = ({\n  styles,\n  defaultMotionStyle, motionStyle,\n}) => (\n  <Motion\n    defaultStyle={defaultMotionStyle}\n    style={motionStyle}\n  >\n  {\n    ({ scale }) => (\n      <div\n        className={styles.marker}\n        style={{\n          transform: `translate3D(0,0,0) scale(${scale}, ${scale})`,\n        }}\n      >\n      </div>\n    )\n  }\n  </Motion>\n);\n\nexport const simpleMarkerHOC = compose(\n  defaultProps({\n    styles: simpleMarkerStyles,\n    initialScale: 0.3,\n    defaultScale: 0.6,\n    hoveredScale: 0.7,\n  }),\n  // resuse HOC\n  clusterMarkerHOC\n);\n\nexport default simpleMarkerHOC(simpleMarker);\n"
  },
  {
    "path": "src/markers/SimpleMarker.sass",
    "content": "\n$markerWidth: 49px\n$markerHeight: 64px\n\n$originX: $markerWidth * .5\n$originY: $markerHeight\n\n.marker\n  background-image: url('./mapIcon.svg')\n  position: absolute\n  cursor: pointer\n  width: $markerWidth\n  height: $markerHeight\n  top: -$originY\n  left: -$originX\n  transform-origin: $originX $originY\n  margin: 0\n  padding: 0\n"
  }
]