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