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 (
);
}
}
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 }) => (
)
}
);
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