├── .env.example ├── index.js ├── examples ├── screenshots │ └── circle.png ├── global.styles.css ├── components │ ├── basic.js │ ├── autocomplete.module.css │ ├── withMarkers.js │ ├── withRectangle.js │ ├── withPolygons.js │ ├── withPolylines.js │ ├── resizeEvent.js │ ├── places.js │ ├── withHeatMap.js │ ├── clickableMarkers.js │ └── autocomplete.js ├── styles.module.css ├── Container.js └── index.js ├── .gitignore ├── resources └── readme │ ├── fullstack-react-hero-book.png │ ├── fullstackreact-google-maps-tutorial.png │ └── dolphins-badge-ff00ff.svg ├── .npmignore ├── src ├── lib │ ├── String.js │ ├── windowOrGlobal.js │ ├── cancelablePromise.js │ ├── areBoundsEqual.js │ ├── arePathsEqual.js │ ├── GoogleApi.js │ └── ScriptCache.js ├── __tests__ │ ├── lib │ │ ├── String.spec.js │ │ ├── ScriptCache.spec.js │ │ ├── GoogleApi.spec.js │ │ └── arePathsEqual.spec.js │ ├── index.js │ ├── components │ │ └── Marker.spec.js │ └── GoogleApiComponent.spec.js ├── components │ ├── Polyline.js │ ├── Polygon.js │ ├── Marker.js │ ├── HeatMap.js │ ├── InfoWindow.js │ ├── Rectangle.js │ └── Circle.js ├── GoogleApiComponent.js └── index.js ├── .babelrc ├── scripts ├── prepublish.sh └── mocha_runner.js ├── Makefile ├── LICENSE ├── .github └── workflows │ └── npmpublish.yml ├── .eslintrc ├── package.json ├── webpack.config.js ├── index.d.ts └── README.md /.env.example: -------------------------------------------------------------------------------- 1 | GAPI_KEY=FILL_IN_YOUR_KEY_HERE 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/index'); -------------------------------------------------------------------------------- /examples/screenshots/circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackreact/google-maps-react/HEAD/examples/screenshots/circle.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *~ 3 | *.iml 4 | .*.haste_cache.* 5 | .DS_Store 6 | .idea 7 | npm-debug.log 8 | node_modules 9 | .env 10 | public/ 11 | -------------------------------------------------------------------------------- /resources/readme/fullstack-react-hero-book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackreact/google-maps-react/HEAD/resources/readme/fullstack-react-hero-book.png -------------------------------------------------------------------------------- /resources/readme/fullstackreact-google-maps-tutorial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackreact/google-maps-react/HEAD/resources/readme/fullstackreact-google-maps-tutorial.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *~ 3 | *.iml 4 | .*.haste_cache.* 5 | .DS_Store 6 | .idea 7 | .babelrc 8 | .eslintrc 9 | npm-debug.log 10 | src/ 11 | examples/ 12 | public/ 13 | scripts/ 14 | -------------------------------------------------------------------------------- /examples/global.styles.css: -------------------------------------------------------------------------------- 1 | @import url("../node_modules/highlight.js/styles/github.css"); 2 | 3 | #readme { 4 | padding: 10px; 5 | 6 | img { 7 | width: 100%; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/String.js: -------------------------------------------------------------------------------- 1 | export const camelize = function(str) { 2 | return str.split('_').map(function(word) { 3 | return word.charAt(0).toUpperCase() + word.slice(1); 4 | }).join(''); 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/windowOrGlobal.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (typeof self === 'object' && self.self === self && self) || 4 | (typeof global === 'object' && global.global === global && global) || 5 | this 6 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | }, 7 | "test": { 8 | "presets": [] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /scripts/prepublish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "=> Transpiling..." 4 | echo "" 5 | export NODE_ENV=production 6 | rm -rf ./dist 7 | ./node_modules/.bin/babel \ 8 | --plugins 'transform-es2015-modules-umd' \ 9 | --presets 'stage-0,react' \ 10 | --ignore __tests__ \ 11 | --out-dir ./dist \ 12 | src 13 | echo "" 14 | echo "=> Complete" 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: publish dev test example 2 | 3 | dev: 4 | npm run dev 5 | 6 | build: 7 | npm run prepublish 8 | 9 | publish: 10 | npm version patch 11 | npm publish . 12 | 13 | test: 14 | npm run test 15 | 16 | testwatch: 17 | npm run test-watch 18 | 19 | example: build 20 | npm run build 21 | 22 | publish_pages: example 23 | gh-pages -d ./public 24 | -------------------------------------------------------------------------------- /src/__tests__/lib/String.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {expect} from 'chai'; 4 | import sinon from 'sinon'; 5 | 6 | import { camelize } from '../../lib/String' 7 | 8 | describe('string', () => { 9 | it('camelizes words', () => { 10 | expect(camelize('hello world')).to.equal('HelloWorld'); 11 | expect(camelize('mousemove')).to.equal('Mousemove'); 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /examples/components/basic.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Map from '../../src/index'; 4 | 5 | const Container = props => { 6 | if (!props.loaded) return
Loading...
; 7 | 8 | return ( 9 | 16 | ); 17 | }; 18 | 19 | export default Container; 20 | -------------------------------------------------------------------------------- /src/lib/cancelablePromise.js: -------------------------------------------------------------------------------- 1 | // https://facebook.github.io/react/blog/2015/12/16/ismounted-antipattern.html 2 | 3 | export const makeCancelable = (promise) => { 4 | let hasCanceled_ = false; 5 | 6 | const wrappedPromise = new Promise((resolve, reject) => { 7 | promise.then((val) => 8 | hasCanceled_ ? reject({isCanceled: true}) : resolve(val) 9 | ); 10 | promise.catch((error) => 11 | hasCanceled_ ? reject({isCanceled: true}) : reject(error) 12 | ); 13 | }); 14 | 15 | return { 16 | promise: wrappedPromise, 17 | cancel() { 18 | hasCanceled_ = true; 19 | }, 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /src/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {shallow, mount, render} from 'enzyme'; 4 | import {expect} from 'chai'; 5 | import sinon from 'sinon'; 6 | 7 | import Map from '../index'; 8 | 9 | describe('Map', () => { 10 | let wrapper; 11 | 12 | describe('google prop', () => { 13 | it('explodes without a `google` prop', () => { 14 | expect(() => mount()).to.throw(Error); 15 | }); 16 | 17 | it('does not explode with a `google` prop', () => { 18 | expect(() => mount( 19 | 21 | )).not.to.throw(Error); 22 | }); 23 | }); 24 | 25 | }) 26 | -------------------------------------------------------------------------------- /resources/readme/dolphins-badge-ff00ff.svg: -------------------------------------------------------------------------------- 1 | 🐬🐬dolphinsdolphins -------------------------------------------------------------------------------- /examples/components/autocomplete.module.css: -------------------------------------------------------------------------------- 1 | .flexWrapper { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: space-between; 5 | 6 | .left { 7 | flex: 1; 8 | order: 1; 9 | padding: 20px; 10 | } 11 | 12 | .right { 13 | flex: 1; 14 | order: 2; 15 | } 16 | 17 | form { 18 | margin: 20px auto; 19 | position: relative; 20 | height: 30px; 21 | 22 | input { 23 | height: 100%; 24 | font-size: 1.4em; 25 | } 26 | 27 | input[type=text] { 28 | width: 80%; 29 | position: absolute; 30 | top: 0; 31 | left: 0; 32 | } 33 | input[type=submit] { 34 | width: 20%; 35 | position: absolute; 36 | right: 0; 37 | top: 2px; 38 | border: 1px solid #ddd; 39 | background: #FFF; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/components/withMarkers.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Map from '../../src/index'; 4 | 5 | import Marker from '../../src/components/Marker'; 6 | import InfoWindow from '../../src/components/InfoWindow'; 7 | 8 | const WithMarkers = props => { 9 | if (!props.loaded) return
Loading...
; 10 | 11 | return ( 12 | 17 | 22 | 23 | 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default WithMarkers; 34 | -------------------------------------------------------------------------------- /examples/components/withRectangle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Map from '../../src/index'; 4 | 5 | import Rectangle from '../../src/components/Rectangle'; 6 | 7 | 8 | const WithRectangles = props => { 9 | if (!props.loaded) return
Loading...
; 10 | 11 | const bounds = { 12 | north: 37.789411, 13 | south: 37.731757, 14 | east: -122.410333, 15 | west: -122.489116, 16 | }; 17 | 18 | return ( 19 | 25 | 33 | 34 | ); 35 | }; 36 | 37 | export default WithRectangles; 38 | -------------------------------------------------------------------------------- /examples/components/withPolygons.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Map from '../../src/index'; 4 | 5 | import Polygon from '../../src/components/Polygon'; 6 | 7 | const WithPolygons = props => { 8 | if (!props.loaded) return
Loading...
; 9 | 10 | const polygon = [ 11 | { lat: 37.789411, lng: -122.422116 }, 12 | { lat: 37.785757, lng: -122.421333 }, 13 | { lat: 37.789352, lng: -122.415346 } 14 | ]; 15 | 16 | return ( 17 | 22 | 30 | 31 | ); 32 | }; 33 | 34 | export default WithPolygons; 35 | -------------------------------------------------------------------------------- /examples/components/withPolylines.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Map from '../../src/index'; 4 | 5 | import Polyline from '../../src/components/Polyline'; 6 | 7 | const WithPolylines = props => { 8 | if (!props.loaded) return
Loading...
; 9 | 10 | const polyline = [ 11 | { lat: 37.789411, lng: -122.422116 }, 12 | { lat: 37.785757, lng: -122.421333 }, 13 | { lat: 37.789352, lng: -122.415346 } 14 | ]; 15 | 16 | return ( 17 | 22 | 30 | 31 | ); 32 | }; 33 | 34 | export default WithPolylines; 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Fullstack.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 12 15 | - run: npm ci 16 | - run: npm test 17 | 18 | publish-npm: 19 | needs: build 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v2 23 | - uses: actions/setup-node@v1 24 | with: 25 | node-version: 12 26 | registry-url: https://registry.npmjs.org/ 27 | - run: npm ci 28 | - run: npm publish 29 | env: 30 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 31 | 32 | publish-gpr: 33 | needs: build 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v2 37 | - uses: actions/setup-node@v1 38 | with: 39 | node-version: 12 40 | registry-url: https://npm.pkg.github.com/ 41 | scope: '@your-github-username' 42 | - run: npm ci 43 | - run: npm publish 44 | env: 45 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 46 | -------------------------------------------------------------------------------- /src/lib/areBoundsEqual.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Compares two bound objects. 3 | */ 4 | 5 | export const areBoundsEqual = function(boundA, boundB) { 6 | if (boundA === boundB) { 7 | return true; 8 | } 9 | if ( 10 | !(boundA instanceof Object) || 11 | !(boundB instanceof Object) 12 | ) { 13 | return false; 14 | } 15 | if (Object.keys(boundA).length !== Object.keys(boundB).length) { 16 | return false; 17 | } 18 | if ( 19 | !areValidBounds(boundA) || 20 | !areValidBounds(boundB) 21 | ) { 22 | return false; 23 | } 24 | for (const key of Object.keys(boundA)) { 25 | if (boundA[key] !== boundB[key]) { 26 | return false; 27 | } 28 | } 29 | return true; 30 | }; 31 | 32 | /** 33 | * Helper that checks whether an array consists of objects 34 | * with lat and lng properties 35 | * @param {object} elem the element to check 36 | * @returns {boolean} whether or not it's valid 37 | */ 38 | const areValidBounds = function(elem) { 39 | return ( 40 | elem !== null && 41 | typeof elem === 'object' && 42 | elem.hasOwnProperty('north') && 43 | elem.hasOwnProperty('south') && 44 | elem.hasOwnProperty('east') && 45 | elem.hasOwnProperty('west') 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /src/lib/arePathsEqual.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Compares two path arrays of LatLng objects. 3 | */ 4 | 5 | export const arePathsEqual = function(pathA, pathB) { 6 | if (pathA === pathB) { 7 | return true; 8 | } 9 | if (!Array.isArray(pathA) || !Array.isArray(pathB)) { 10 | return false; 11 | } 12 | if (pathA.length !== pathB.length) { 13 | return false; 14 | } 15 | for (let i = 0; i < pathA.length; ++i) { 16 | if (pathA[i] === pathB[i]) { 17 | continue; 18 | } 19 | if ( 20 | !isValidLatLng(pathA[i]) || 21 | !isValidLatLng(pathB[i]) 22 | ) { 23 | return false; 24 | } 25 | if ( 26 | pathB[i].lat !== pathA[i].lat || 27 | pathB[i].lng !== pathA[i].lng 28 | ) { 29 | return false; 30 | } 31 | } 32 | return true; 33 | } 34 | 35 | /** 36 | * Helper that checks whether an array consists of objects 37 | * with lat and lng properties 38 | * @param {object} elem the element to check 39 | * @returns {boolean} whether or not it's valid 40 | */ 41 | const isValidLatLng = function(elem) { 42 | return ( 43 | elem !== null && 44 | typeof elem === 'object' && 45 | elem.hasOwnProperty('lat') && 46 | elem.hasOwnProperty('lng') 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /examples/components/resizeEvent.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import Map from '../../src/index'; 4 | 5 | class Container extends Component { 6 | state = { 7 | showingInfoWindow: false 8 | }; 9 | 10 | onMapReady = (mapProps, map) => { 11 | this.map = map; 12 | 13 | window.onresize = () => { 14 | const currCenter = map.getCenter(); 15 | this.props.google.maps.event.trigger(map, 'resize'); 16 | map.setCenter(currCenter); 17 | }; 18 | }; 19 | 20 | onMarkerClick = () => this.setState({ showingInfoWindow: true }); 21 | 22 | onInfoWindowClose = () => this.setState({ showingInfoWindow: false }); 23 | 24 | onMapClicked = () => { 25 | if (this.state.showingInfoWindow) 26 | this.setState({ showingInfoWindow: false }); 27 | }; 28 | 29 | render() { 30 | if (!this.props.loaded) return
Loading...
; 31 | 32 | return ( 33 | 42 | ); 43 | } 44 | } 45 | 46 | export default Container; 47 | -------------------------------------------------------------------------------- /examples/components/places.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import Map from '../../src/index'; 4 | 5 | const Listing = ({ places }) => ( 6 |
    {places && places.map(p =>
  • {p.name}
  • )}
7 | ); 8 | 9 | class Container extends Component { 10 | state = { 11 | places: [] 12 | }; 13 | 14 | onMapReady = (mapProps, map) => this.searchNearby(map, map.center); 15 | 16 | searchNearby = (map, center) => { 17 | const { google } = this.props; 18 | 19 | const service = new google.maps.places.PlacesService(map); 20 | 21 | // Specify location, radius and place types for your Places API search. 22 | const request = { 23 | location: center, 24 | radius: '500', 25 | type: ['food'] 26 | }; 27 | 28 | service.nearbySearch(request, (results, status) => { 29 | if (status === google.maps.places.PlacesServiceStatus.OK) 30 | this.setState({ places: results }); 31 | }); 32 | }; 33 | 34 | render() { 35 | if (!this.props.loaded) return
Loading...
; 36 | 37 | return ( 38 | 43 | 44 | 45 | ); 46 | } 47 | } 48 | 49 | export default Container; 50 | -------------------------------------------------------------------------------- /src/lib/GoogleApi.js: -------------------------------------------------------------------------------- 1 | export const GoogleApi = function(opts) { 2 | opts = opts || {}; 3 | 4 | if (!opts.hasOwnProperty('apiKey')) { 5 | throw new Error('You must pass an apiKey to use GoogleApi'); 6 | } 7 | 8 | const apiKey = opts.apiKey; 9 | const libraries = opts.libraries || ['places']; 10 | const client = opts.client; 11 | const URL = opts.url || 'https://maps.googleapis.com/maps/api/js'; 12 | 13 | const googleVersion = opts.version || '3.31'; 14 | 15 | let script = null; 16 | let google = (typeof window !== 'undefined' && window.google) || null; 17 | let loading = false; 18 | let channel = null; 19 | let language = opts.language; 20 | let region = opts.region || null; 21 | 22 | let onLoadEvents = []; 23 | 24 | const url = () => { 25 | let url = URL; 26 | let params = { 27 | key: apiKey, 28 | callback: 'CALLBACK_NAME', 29 | libraries: libraries.join(','), 30 | client: client, 31 | v: googleVersion, 32 | channel: channel, 33 | language: language, 34 | region: region, 35 | onerror: 'ERROR_FUNCTION' 36 | }; 37 | 38 | let paramStr = Object.keys(params) 39 | .filter(k => !!params[k]) 40 | .map(k => `${k}=${params[k]}`) 41 | .join('&'); 42 | 43 | return `${url}?${paramStr}`; 44 | }; 45 | 46 | return url(); 47 | }; 48 | 49 | export default GoogleApi; 50 | -------------------------------------------------------------------------------- /src/__tests__/lib/ScriptCache.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon' 2 | 3 | import { 4 | shallow, 5 | mount, 6 | render 7 | } from 'enzyme'; 8 | import { 9 | expect 10 | } from 'chai'; 11 | 12 | const createCache = (obj, newElement) => { 13 | const ScriptCache = require('../../lib/ScriptCache').ScriptCache; 14 | let cache = ScriptCache(obj); 15 | cache._scriptTag = sinon.spy(cache, '_scriptTag') 16 | return cache; 17 | } 18 | describe('Cache', () => { 19 | let cache; 20 | let newElement; 21 | let onLoad; 22 | let stub, i; 23 | 24 | beforeEach(() => { 25 | newElement = {}; 26 | cache = createCache({ 27 | example: 'http://example.com' 28 | }, newElement); 29 | }) 30 | 31 | afterEach(() => { 32 | cache._scriptTag.restore(); 33 | }) 34 | 35 | it('adds a script tag', () => { 36 | expect(global.window._scriptMap.has('example')).to.be.ok; 37 | }) 38 | 39 | it('only adds a single script', () => { 40 | createCache({ 41 | example: 'http://example.com' 42 | }) 43 | createCache({ 44 | example: 'http://example.com' 45 | }) 46 | 47 | expect(global.window._scriptMap.has('example')).to.be.ok; 48 | const scripts = global.document.querySelectorAll('script') 49 | expect(scripts.length).to.equal(1); 50 | expect(scripts[0].src).to.equal('http://example.com/') 51 | }); 52 | }) 53 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "es6": true 7 | }, 8 | "ecmaFeatures": { 9 | "modules": true 10 | }, 11 | "rules": { 12 | "no-bitwise": 2, 13 | "no-else-return": 2, 14 | "no-eq-null": 2, 15 | "no-extra-parens": 0, 16 | "no-floating-decimal": 2, 17 | "no-inner-declarations": [2, "both"], 18 | "no-lonely-if": 2, 19 | "no-multiple-empty-lines": [2, {"max": 3}], 20 | "no-self-compare": 2, 21 | "no-underscore-dangle": 0, 22 | "no-use-before-define": 0, 23 | "no-unused-expressions": 0, 24 | "no-void": 2, 25 | "brace-style": [2, "1tbs"], 26 | "camelcase": [1, {"properties": "never"}], 27 | "consistent-return": 0, 28 | "comma-style": [2, "last"], 29 | "complexity": [1, 12], 30 | "func-names": 0, 31 | "guard-for-in": 2, 32 | "max-len": [0, 120, 4], 33 | "new-cap": [2, {"newIsCap": true, "capIsNew": false}], 34 | "quotes": [2, "single"], 35 | "keyword-spacing": [2, {"before": true, "after": true}], 36 | "space-before-blocks": [2, "always"], 37 | "array-bracket-spacing": [2, "never"], 38 | "space-in-parens": [2, "never"], 39 | "strict": [0], 40 | "valid-jsdoc": 2, 41 | "wrap-iife": [2, "any"], 42 | "yoda": [1, "never"] 43 | }, 44 | "plugins": [ 45 | "react" 46 | ], 47 | "globals": { 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/styles.module.css: -------------------------------------------------------------------------------- 1 | $dominantColor: #1BCAFF; 2 | $listColor: #333; 3 | $textColor: #333; 4 | 5 | @import url(https://fonts.googleapis.com/css?family=Open+Sans:400,300); 6 | 7 | html, body { 8 | height: 100%; 9 | margin: 0; 10 | padding: 0; 11 | } 12 | .container { 13 | font-family: 'Open Sans', sans-serif; 14 | font-weight: lighter; 15 | } 16 | .header { 17 | font-size: 0.5em; 18 | background: color($dominantColor a(80%)); 19 | padding: 10px; 20 | margin: 0; 21 | color: $textColor; 22 | text-align: center; 23 | border-bottom: 2px solid color($dominantColor blackness(40%)); 24 | } 25 | .wrapper { 26 | display: flex; 27 | flex-direction: row; 28 | position: relative; 29 | min-height: 100vh; 30 | 31 | .list { 32 | flex: 1; 33 | order: 1; 34 | margin: 0; 35 | padding: 0; 36 | background: color($listColor lightness(25%)); 37 | border-right: 2px solid color($dominantColor blackness(40%)); 38 | ul { 39 | font-size: 1.5em; 40 | padding: 0; 41 | margin: 0; 42 | a { 43 | color: color($textColor contrast(60%)); 44 | text-decoration: none; 45 | } 46 | .active { 47 | color: color($textColor contrast(90%)); 48 | li { 49 | background: color($listColor a(80%)); 50 | } 51 | } 52 | li { 53 | list-style-type: none; 54 | padding: 10px 20px; 55 | &:hover { 56 | background: color($dominantColor a(20%)); 57 | } 58 | } 59 | } 60 | } 61 | 62 | .content { 63 | flex: 3; 64 | order: 2; 65 | position: relative; 66 | min-height: 100%; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/__tests__/lib/GoogleApi.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {shallow, mount, render} from 'enzyme'; 4 | import {expect} from 'chai'; 5 | import sinon from 'sinon'; 6 | 7 | import GoogleApi from '../../lib/GoogleApi' 8 | 9 | const base = 'https://maps.googleapis.com/maps/api/js' 10 | 11 | describe('GoogleApi', () => { 12 | it('loads a url from google api', () => { 13 | expect(GoogleApi({apiKey: '123'}).indexOf(base)).to.be.at.least(0); 14 | }); 15 | 16 | describe('apiKey', () => { 17 | it('appends the apiKey to the url', () => { 18 | expect(GoogleApi({apiKey: 'abc-123-456'}).indexOf('abc-123-456')).to.be.at.least(0); 19 | }); 20 | 21 | it('explodes if no apiKey is given as an option', () => { 22 | expect(() => GoogleApi()).to.throw(Error); 23 | }) 24 | }) 25 | 26 | describe('libraries', () => { 27 | let url; 28 | beforeEach(() => { 29 | url = GoogleApi({ 30 | apiKey: 'abc-123-456', 31 | libraries: ['places', 'people', 'animals'] 32 | }) 33 | }) 34 | 35 | it('adds libraries', () => { 36 | expect(url.indexOf('places,people,animals')).to.be.at.least(0); 37 | }); 38 | 39 | it('includes places library by default', () => { 40 | url = GoogleApi({apiKey: 'abc-123-456'}); 41 | expect(url.indexOf('places')).to.be.at.least(0); 42 | }) 43 | }) 44 | 45 | describe('version', () => { 46 | it('adds the google version', () => { 47 | const url = GoogleApi({apiKey: 'abc-123-456', version: '2016'}); 48 | expect(url.indexOf('v=2016')).to.be.above(0); 49 | }) 50 | }) 51 | 52 | }) 53 | -------------------------------------------------------------------------------- /examples/components/withHeatMap.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Map from '../../src/index'; 4 | import HeatMap from '../../src/components/HeatMap'; 5 | 6 | const WithHeatMap = props => { 7 | if (!props.loaded) return
Loading...
; 8 | 9 | const gradient = [ 10 | 'rgba(0, 255, 255, 0)', 11 | 'rgba(0, 255, 255, 1)', 12 | 'rgba(0, 191, 255, 1)', 13 | 'rgba(0, 127, 255, 1)', 14 | 'rgba(0, 63, 255, 1)', 15 | 'rgba(0, 0, 255, 1)', 16 | 'rgba(0, 0, 223, 1)', 17 | 'rgba(0, 0, 191, 1)', 18 | 'rgba(0, 0, 159, 1)', 19 | 'rgba(0, 0, 127, 1)', 20 | 'rgba(63, 0, 91, 1)', 21 | 'rgba(127, 0, 63, 1)', 22 | 'rgba(191, 0, 31, 1)', 23 | 'rgba(255, 0, 0, 1)' 24 | ]; 25 | 26 | const positions = [ 27 | { lat: 37.782551, lng: -122.445368 }, 28 | { lat: 37.782745, lng: -122.444586 }, 29 | { lat: 37.782842, lng: -122.443688 }, 30 | { lat: 37.782919, lng: -122.442815 }, 31 | { lat: 37.782992, lng: -122.442112 }, 32 | { lat: 37.7831, lng: -122.441461 }, 33 | { lat: 37.783206, lng: -122.440829 }, 34 | { lat: 37.783273, lng: -122.440324 }, 35 | { lat: 37.783316, lng: -122.440023 }, 36 | { lat: 37.783357, lng: -122.439794 }, 37 | { lat: 37.783371, lng: -122.439687 }, 38 | { lat: 37.783368, lng: -122.439666 }, 39 | { lat: 37.783383, lng: -122.439594 }, 40 | { lat: 37.783508, lng: -122.439525 }, 41 | { lat: 37.783842, lng: -122.439591 }, 42 | { lat: 37.784147, lng: -122.439668 } 43 | ]; 44 | 45 | return ( 46 | 51 | 57 | 58 | ); 59 | }; 60 | 61 | export default WithHeatMap; 62 | -------------------------------------------------------------------------------- /src/__tests__/components/Marker.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {shallow, mount, render} from 'enzyme'; 4 | import {expect} from 'chai'; 5 | import sinon from 'sinon'; 6 | 7 | import Marker from '../../components/Marker'; 8 | 9 | // let google = {}; 10 | // google.maps = {}; 11 | // google.maps.LatLng = function(lat, lng, opt_noWrap) {}; 12 | 13 | describe('Marker', () => { 14 | let map = null, google = global.google; 15 | let sandbox; 16 | let LatLng = null; 17 | let location; 18 | 19 | beforeEach(() => { 20 | sandbox = sinon.sandbox.create(); 21 | 22 | map = {} 23 | location = {lat: 37.759703, lng: -122.428093} 24 | 25 | sandbox.stub(google.maps, 'Map').returns(google.maps.Map); 26 | // sandbox.stub(google.maps, 'Marker').returns(google.maps.Marker); 27 | }) 28 | 29 | afterEach(() => { 30 | sandbox.restore(); 31 | }) 32 | 33 | it('accepts a `map` and a `google` prop', () => { 34 | const wrapper = mount(); 37 | expect(wrapper.props().google).to.equal(google); 38 | expect(wrapper.props().map).to.equal(map); 39 | }); 40 | 41 | describe('LatLng', () => { 42 | let wrapper; 43 | beforeEach(() => { 44 | sandbox.stub(google.maps, 'LatLng') 45 | .returns(sinon.createStubInstance(google.maps.LatLng)); 46 | sandbox.spy(google.maps, 'Marker') 47 | wrapper = mount(); 49 | }); 50 | 51 | it('creates a location from the position prop', () => { 52 | wrapper.setProps({map: map}) 53 | sinon.assert 54 | .calledWith(google.maps.LatLng, location.lat, location.lng) 55 | }); 56 | 57 | it('creates a Marker from the position prop', () => { 58 | wrapper.setProps({map: map}) 59 | sinon.assert.called(google.maps.Marker) 60 | }); 61 | 62 | }) 63 | 64 | }) 65 | -------------------------------------------------------------------------------- /scripts/mocha_runner.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register'); 2 | require('babel-polyfill'); 3 | 4 | var jsdom = require('jsdom').jsdom; 5 | var chai = require('chai'), 6 | spies = require('chai-spies'); 7 | var sinon = require('sinon'); 8 | 9 | var exposedProperties = ['window', 'navigator', 'document']; 10 | 11 | global.document = jsdom(''); 12 | global.window = document.defaultView; 13 | Object.keys(document.defaultView).forEach((property) => { 14 | if (typeof global[property] === 'undefined') { 15 | exposedProperties.push(property); 16 | global[property] = document.defaultView[property]; 17 | } 18 | }); 19 | 20 | global.navigator = { 21 | userAgent: 'node.js' 22 | }; 23 | 24 | chai.use(spies); 25 | 26 | const google = { 27 | maps: { 28 | LatLng: function(lat, lng) { 29 | return { 30 | latitude: parseFloat(lat), 31 | longitude: parseFloat(lng), 32 | 33 | lat: function() { 34 | return this.latitude; 35 | }, 36 | lng: function() { 37 | return this.longitude; 38 | } 39 | }; 40 | }, 41 | LatLngBounds: function(ne, sw) { 42 | return { 43 | getSouthWest: function() { 44 | return sw; 45 | }, 46 | getNorthEast: function() { 47 | return ne; 48 | } 49 | }; 50 | }, 51 | OverlayView: function() { 52 | return {}; 53 | }, 54 | InfoWindow: function() { 55 | return {}; 56 | }, 57 | Marker: function() { 58 | return { 59 | addListener: function() {}, 60 | setMap: function() {} 61 | }; 62 | }, 63 | MarkerImage: function() { 64 | return {}; 65 | }, 66 | Map: function() { 67 | return { 68 | addListener: function() {}, 69 | trigger: function() {} 70 | }; 71 | }, 72 | Point: function() { 73 | return {}; 74 | }, 75 | Size: function() { 76 | return {}; 77 | }, 78 | event: { 79 | trigger: function() {} 80 | } 81 | } 82 | }; 83 | 84 | global.google = google; 85 | 86 | documentRef = document; 87 | -------------------------------------------------------------------------------- /src/__tests__/GoogleApiComponent.spec.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // 3 | // import {shallow, mount, render} from 'enzyme'; 4 | // import {expect} from 'chai'; 5 | // import sinon from 'sinon'; 6 | // 7 | // import {ScriptCache} from '../lib/ScriptCache' 8 | // import GoogleApiComponent from '../GoogleApiComponent' 9 | // 10 | // const C = React.createClass({ 11 | // render: function() { 12 | // return ( 13 | //
Sample component
14 | // ) 15 | // } 16 | // }) 17 | // 18 | // const createCache = (res) => (obj) => { 19 | // let cache = ScriptCache(global)(obj); 20 | // sinon.stub(cache, '_scriptTag').returns(res) 21 | // return cache; 22 | // } 23 | // 24 | // const apiKey = 'abc-123' 25 | // const newElement = {}; 26 | // const Wrapped = GoogleApiComponent({apiKey: apiKey, createCache: createCache(newElement)})(C); 27 | // 28 | // // const jsdom = require('jsdom') 29 | // // global.document = jsdom.jsdom('', { 30 | // // globalize: true, 31 | // // console: true, 32 | // // useEach: false, 33 | // // skipWindowCheck: false, 34 | // // }); 35 | // 36 | // describe('GoogleApiComponent', () => { 37 | // let wrapper; 38 | // 39 | // beforeEach(() => { 40 | // wrapper = shallow() 41 | // }) 42 | // 43 | // it('loads the component', () => { 44 | // expect(wrapper.find('div').length).to.be.at.least(1); 45 | // }); 46 | // 47 | // describe('map props', () => { 48 | // let wrapped; 49 | // beforeEach(() => { 50 | // wrapper = mount() 51 | // wrapped = wrapper.childAt(0); 52 | // }) 53 | // 54 | // it('adds a loading prop', () => { 55 | // expect(wrapped.props().loaded).to.be.falsy; 56 | // }) 57 | // 58 | // it('adds a `google` prop', () => { 59 | // expect(wrapped.props().google).to.be.null; 60 | // }); 61 | // 62 | // it('adds a `map` prop', () => { 63 | // expect(wrapped.props().map).to.be.null; 64 | // }) 65 | // 66 | // describe('onLoad', () => {}) 67 | // 68 | // }) 69 | // 70 | // }) 71 | -------------------------------------------------------------------------------- /examples/components/clickableMarkers.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import Map from '../../src/index'; 4 | 5 | import InfoWindow from '../../src/components/InfoWindow'; 6 | import Marker from '../../src/components/Marker'; 7 | 8 | class WithMarkers extends Component { 9 | state = { 10 | activeMarker: {}, 11 | selectedPlace: {}, 12 | showingInfoWindow: false 13 | }; 14 | 15 | onMarkerClick = (props, marker) => 16 | this.setState({ 17 | activeMarker: marker, 18 | selectedPlace: props, 19 | showingInfoWindow: true 20 | }); 21 | 22 | onInfoWindowClose = () => 23 | this.setState({ 24 | activeMarker: null, 25 | showingInfoWindow: false 26 | }); 27 | 28 | onMapClicked = () => { 29 | if (this.state.showingInfoWindow) 30 | this.setState({ 31 | activeMarker: null, 32 | showingInfoWindow: false 33 | }); 34 | }; 35 | 36 | render() { 37 | if (!this.props.loaded) return
Loading...
; 38 | 39 | return ( 40 | 46 | 51 | 52 | 57 | 58 | 59 | 60 | 64 |
65 |

{this.state.selectedPlace.name}

66 |
67 |
68 | 69 | 70 | 71 | Click on any of the markers to display an additional info. 72 | 73 | 74 |
75 | ); 76 | } 77 | } 78 | 79 | export default WithMarkers; 80 | -------------------------------------------------------------------------------- /examples/components/autocomplete.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import Map, { Marker } from '../../src/index'; 4 | 5 | import styles from './autocomplete.module.css'; 6 | 7 | class Contents extends Component { 8 | state = { 9 | position: null 10 | }; 11 | 12 | componentDidMount() { 13 | this.renderAutoComplete(); 14 | } 15 | 16 | componentDidUpdate(prevProps) { 17 | if (this.props !== prevProps.map) this.renderAutoComplete(); 18 | } 19 | 20 | onSubmit(e) { 21 | e.preventDefault(); 22 | } 23 | 24 | renderAutoComplete() { 25 | const { google, map } = this.props; 26 | 27 | if (!google || !map) return; 28 | 29 | const autocomplete = new google.maps.places.Autocomplete(this.autocomplete); 30 | autocomplete.bindTo('bounds', map); 31 | 32 | autocomplete.addListener('place_changed', () => { 33 | const place = autocomplete.getPlace(); 34 | 35 | if (!place.geometry) return; 36 | 37 | if (place.geometry.viewport) map.fitBounds(place.geometry.viewport); 38 | else { 39 | map.setCenter(place.geometry.location); 40 | map.setZoom(17); 41 | } 42 | 43 | this.setState({ position: place.geometry.location }); 44 | }); 45 | } 46 | 47 | render() { 48 | const { position } = this.state; 49 | 50 | return ( 51 |
52 |
53 |
54 | (this.autocomplete = ref)} 57 | type="text" 58 | /> 59 | 60 | 61 |
62 | 63 |
64 |
Lat: {position && position.lat()}
65 |
Lng: {position && position.lng()}
66 |
67 |
68 | 69 |
70 | 79 | 80 | 81 |
82 |
83 | ); 84 | } 85 | } 86 | 87 | const MapWrapper = props => ( 88 | 89 | 90 | 91 | ); 92 | 93 | export default MapWrapper; 94 | -------------------------------------------------------------------------------- /examples/Container.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | import GitHubForkRibbon from 'react-github-fork-ribbon'; 4 | import PropTypes from 'prop-types'; 5 | import {withRouter, Switch, Link, Redirect, Route} from 'react-router-dom'; 6 | 7 | import styles from './styles.module.css'; 8 | 9 | const GoogleApiWrapper = __IS_DEV__ 10 | ? require('../src/index').GoogleApiWrapper 11 | : require('../dist').GoogleApiWrapper; 12 | 13 | class Container extends Component { 14 | static propTypes = {}; 15 | 16 | static contextTypes = { 17 | router: PropTypes.object 18 | }; 19 | 20 | render() { 21 | const {children, routes, routeDef} = this.props; 22 | 23 | return ( 24 |
25 | 30 | Fork me on GitHub 31 | 32 | 33 |
34 |
35 |
    36 | {routes.map(route => ( 37 | 38 |
  • {route.name}
  • 39 | 40 | ))} 41 |
42 |
43 | 44 |
45 |
46 |

{routeDef && routeDef.name} Example

47 | 48 |

49 | 50 | Readme 51 | 52 |

53 |
54 | 55 | 56 | {routes.map(route => ( 57 | ( 63 |
64 | 69 |
70 | )} 71 | /> 72 | ))} 73 | 74 |
75 |
76 |
77 |
78 | ); 79 | } 80 | } 81 | 82 | const Loading = () =>
Fancy loading container
; 83 | 84 | export default withRouter( 85 | GoogleApiWrapper({ 86 | apiKey: __GAPI_KEY__, 87 | libraries: ['places', 'visualization'], 88 | LoadingContainer: Loading 89 | })(Container) 90 | ); 91 | -------------------------------------------------------------------------------- /src/components/Polyline.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { arePathsEqual } from '../lib/arePathsEqual'; 5 | import { camelize } from '../lib/String'; 6 | const evtNames = ['click', 'mouseout', 'mouseover']; 7 | 8 | const wrappedPromise = function() { 9 | var wrappedPromise = {}, 10 | promise = new Promise(function 11 | (resolve, reject) { 12 | wrappedPromise.resolve = resolve; 13 | wrappedPromise.reject = reject; 14 | }); 15 | wrappedPromise.then = promise.then.bind(promise); 16 | wrappedPromise.catch = promise.catch.bind(promise); 17 | wrappedPromise.promise = promise; 18 | 19 | return wrappedPromise; 20 | } 21 | 22 | export class Polyline extends React.Component { 23 | componentDidMount() { 24 | this.polylinePromise = wrappedPromise(); 25 | this.renderPolyline(); 26 | } 27 | 28 | componentDidUpdate(prevProps) { 29 | if ( 30 | this.props.map !== prevProps.map || 31 | !arePathsEqual(this.props.path, prevProps.path) 32 | ) { 33 | if (this.polyline) { 34 | this.polyline.setMap(null); 35 | } 36 | this.renderPolyline(); 37 | } 38 | } 39 | 40 | componentWillUnmount() { 41 | if (this.polyline) { 42 | this.polyline.setMap(null); 43 | } 44 | } 45 | 46 | renderPolyline() { 47 | const { 48 | map, 49 | google, 50 | path, 51 | strokeColor, 52 | strokeOpacity, 53 | strokeWeight, 54 | ...props 55 | } = this.props; 56 | 57 | if (!google) { 58 | return null; 59 | } 60 | 61 | const params = { 62 | map, 63 | path, 64 | strokeColor, 65 | strokeOpacity, 66 | strokeWeight, 67 | ...props 68 | }; 69 | 70 | this.polyline = new google.maps.Polyline(params); 71 | 72 | evtNames.forEach(e => { 73 | this.polyline.addListener(e, this.handleEvent(e)); 74 | }); 75 | 76 | this.polylinePromise.resolve(this.polyline); 77 | } 78 | 79 | getPolyline() { 80 | return this.polylinePromise; 81 | } 82 | 83 | handleEvent(evt) { 84 | return (e) => { 85 | const evtName = `on${camelize(evt)}` 86 | if (this.props[evtName]) { 87 | this.props[evtName](this.props, this.polyline, e); 88 | } 89 | } 90 | } 91 | 92 | render() { 93 | return null; 94 | } 95 | } 96 | 97 | Polyline.propTypes = { 98 | path: PropTypes.array, 99 | strokeColor: PropTypes.string, 100 | strokeOpacity: PropTypes.number, 101 | strokeWeight: PropTypes.number 102 | } 103 | 104 | evtNames.forEach(e => Polyline.propTypes[e] = PropTypes.func) 105 | 106 | Polyline.defaultProps = { 107 | name: 'Polyline' 108 | } 109 | 110 | export default Polyline 111 | -------------------------------------------------------------------------------- /src/components/Polygon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { arePathsEqual } from '../lib/arePathsEqual'; 5 | import { camelize } from '../lib/String'; 6 | const evtNames = ['click', 'mouseout', 'mouseover']; 7 | 8 | const wrappedPromise = function() { 9 | var wrappedPromise = {}, 10 | promise = new Promise(function (resolve, reject) { 11 | wrappedPromise.resolve = resolve; 12 | wrappedPromise.reject = reject; 13 | }); 14 | wrappedPromise.then = promise.then.bind(promise); 15 | wrappedPromise.catch = promise.catch.bind(promise); 16 | wrappedPromise.promise = promise; 17 | 18 | return wrappedPromise; 19 | } 20 | 21 | export class Polygon extends React.Component { 22 | componentDidMount() { 23 | this.polygonPromise = wrappedPromise(); 24 | this.renderPolygon(); 25 | } 26 | 27 | componentDidUpdate(prevProps) { 28 | if ( 29 | this.props.map !== prevProps.map || 30 | !arePathsEqual(this.props.paths, prevProps.paths) 31 | ) { 32 | if (this.polygon) { 33 | this.polygon.setMap(null); 34 | } 35 | this.renderPolygon(); 36 | } 37 | } 38 | 39 | componentWillUnmount() { 40 | if (this.polygon) { 41 | this.polygon.setMap(null); 42 | } 43 | } 44 | 45 | renderPolygon() { 46 | const { 47 | map, 48 | google, 49 | paths, 50 | strokeColor, 51 | strokeOpacity, 52 | strokeWeight, 53 | fillColor, 54 | fillOpacity, 55 | ...props 56 | } = this.props; 57 | 58 | if (!google) { 59 | return null; 60 | } 61 | 62 | const params = { 63 | map, 64 | paths, 65 | strokeColor, 66 | strokeOpacity, 67 | strokeWeight, 68 | fillColor, 69 | fillOpacity, 70 | ...props 71 | }; 72 | 73 | this.polygon = new google.maps.Polygon(params); 74 | 75 | evtNames.forEach(e => { 76 | this.polygon.addListener(e, this.handleEvent(e)); 77 | }); 78 | 79 | this.polygonPromise.resolve(this.polygon); 80 | } 81 | 82 | getPolygon() { 83 | return this.polygonPromise; 84 | } 85 | 86 | handleEvent(evt) { 87 | return (e) => { 88 | const evtName = `on${camelize(evt)}` 89 | if (this.props[evtName]) { 90 | this.props[evtName](this.props, this.polygon, e); 91 | } 92 | } 93 | } 94 | 95 | render() { 96 | return null; 97 | } 98 | } 99 | 100 | Polygon.propTypes = { 101 | paths: PropTypes.array, 102 | strokeColor: PropTypes.string, 103 | strokeOpacity: PropTypes.number, 104 | strokeWeight: PropTypes.number, 105 | fillColor: PropTypes.string, 106 | fillOpacity: PropTypes.number 107 | } 108 | 109 | evtNames.forEach(e => Polygon.propTypes[e] = PropTypes.func) 110 | 111 | Polygon.defaultProps = { 112 | name: 'Polygon' 113 | } 114 | 115 | export default Polygon 116 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google-maps-react", 3 | "version": "2.0.8", 4 | "description": "Google maps container", 5 | "author": "Fullstack.io ", 6 | "license": "MIT", 7 | "options": { 8 | "mocha": "--require scripts/mocha_runner -t rewireify src/**/__tests__/**/*.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/fullstackreact/google-maps-react.git" 13 | }, 14 | "main": "dist/index.js", 15 | "files": [ 16 | "dist", 17 | "index.d.ts" 18 | ], 19 | "types": "dist/index.d.ts", 20 | "sideEffects": false, 21 | "scripts": { 22 | "prepublish": "./scripts/prepublish.sh", 23 | "preversion": ". ./scripts/prepublish.sh", 24 | "dev": "NODE_ENV=development ./node_modules/hjs-webpack/bin/hjs-dev-server.js", 25 | "build": "NODE_ENV=production webpack", 26 | "publish_pages": "gh-pages -d public/", 27 | "lint": "eslint ./src", 28 | "lintfix": "eslint ./src --fix", 29 | "testonly": "NODE_ENV=test mocha $npm_package_options_mocha", 30 | "test": "npm run lint && npm run testonly", 31 | "test-watch": "npm run testonly -- --watch --watch-extensions js" 32 | }, 33 | "devDependencies": { 34 | "@types/googlemaps": "^3", 35 | "@types/react": "^15.0.0", 36 | "autoprefixer": "^6.3.6", 37 | "babel-cli": "^6.26.0", 38 | "babel-core": "^6.7.4", 39 | "babel-eslint": "^6.0.2", 40 | "babel-loader": "^6.2.4", 41 | "babel-plugin-transform-es2015-modules-umd": "^6.6.5", 42 | "babel-polyfill": "^6.7.4", 43 | "babel-preset-es2015": "^6.6.0", 44 | "babel-preset-react": "^6.5.0", 45 | "babel-preset-react-hmre": "^1.1.1", 46 | "babel-preset-stage-0": "^6.5.0", 47 | "babel-preset-stage-2": "^6.5.0", 48 | "babel-runtime": "^6.6.1", 49 | "chai": "^3.5.0", 50 | "chai-spies": "^0.7.1", 51 | "css-loader": "^0.23.1", 52 | "cssnano": "^3.5.2", 53 | "dotenv": "^2.0.0", 54 | "enzyme": "^2.2.0", 55 | "eslint": "^2.7.0", 56 | "eslint-plugin-babel": "^3.1.0", 57 | "eslint-plugin-react": "^5.1.1", 58 | "file-loader": "^0.8.5", 59 | "gh-pages": "^1.1.0", 60 | "highlight.js": "^9.3.0", 61 | "history": "^3.0.0", 62 | "hjs-webpack": "^8.1.0", 63 | "jsdom": "^9.2.1", 64 | "marked": "^0.3.17", 65 | "mocha": "^2.4.5", 66 | "nodemon": "^1.9.1", 67 | "npm-font-open-sans": "0.0.3", 68 | "postcss-loader": "^0.9.1", 69 | "precss": "^1.4.0", 70 | "prop-types": "^15.5.10", 71 | "react": "^15.0.0", 72 | "react-addons-test-utils": "^15.0.0", 73 | "react-dom": "^15.0.0", 74 | "react-github-fork-ribbon": "^0.5.1", 75 | "react-router-dom": "^4.2.2", 76 | "sinon": "^1.17.3", 77 | "style-loader": "^0.13.1", 78 | "url-loader": "^0.5.7", 79 | "webpack": "^1.13.0" 80 | }, 81 | "peerDependencies": { 82 | "react": "~0.14.8 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", 83 | "react-dom": "~0.14.8 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" 84 | }, 85 | "dependencies": {} 86 | } 87 | -------------------------------------------------------------------------------- /src/components/Marker.js: -------------------------------------------------------------------------------- 1 | import React, {Fragment} from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { camelize } from '../lib/String' 5 | 6 | const evtNames = [ 7 | 'click', 8 | 'dblclick', 9 | 'dragend', 10 | 'mousedown', 11 | 'mouseout', 12 | 'mouseover', 13 | 'mouseup', 14 | 'recenter', 15 | ]; 16 | 17 | const wrappedPromise = function() { 18 | var wrappedPromise = {}, 19 | promise = new Promise(function (resolve, reject) { 20 | wrappedPromise.resolve = resolve; 21 | wrappedPromise.reject = reject; 22 | }); 23 | wrappedPromise.then = promise.then.bind(promise); 24 | wrappedPromise.catch = promise.catch.bind(promise); 25 | wrappedPromise.promise = promise; 26 | 27 | return wrappedPromise; 28 | } 29 | 30 | export class Marker extends React.Component { 31 | 32 | componentDidMount() { 33 | this.markerPromise = wrappedPromise(); 34 | this.renderMarker(); 35 | } 36 | 37 | componentDidUpdate(prevProps) { 38 | if ((this.props.map !== prevProps.map) || 39 | (this.props.position !== prevProps.position) || 40 | (this.props.icon !== prevProps.icon)) { 41 | if (this.marker) { 42 | this.marker.setMap(null); 43 | } 44 | this.renderMarker(); 45 | } 46 | } 47 | 48 | componentWillUnmount() { 49 | if (this.marker) { 50 | this.marker.setMap(null); 51 | } 52 | } 53 | 54 | renderMarker() { 55 | const { 56 | map, 57 | google, 58 | position, 59 | mapCenter, 60 | icon, 61 | label, 62 | draggable, 63 | title, 64 | ...props 65 | } = this.props; 66 | if (!google) { 67 | return null 68 | } 69 | 70 | let pos = position || mapCenter; 71 | if (!(pos instanceof google.maps.LatLng)) { 72 | pos = new google.maps.LatLng(pos.lat, pos.lng); 73 | } 74 | 75 | const pref = { 76 | map, 77 | position: pos, 78 | icon, 79 | label, 80 | title, 81 | draggable, 82 | ...props 83 | }; 84 | this.marker = new google.maps.Marker(pref); 85 | 86 | evtNames.forEach(e => { 87 | this.marker.addListener(e, this.handleEvent(e)); 88 | }); 89 | 90 | this.markerPromise.resolve(this.marker); 91 | } 92 | 93 | getMarker() { 94 | return this.markerPromise; 95 | } 96 | 97 | handleEvent(evt) { 98 | return (e) => { 99 | const evtName = `on${camelize(evt)}` 100 | if (this.props[evtName]) { 101 | this.props[evtName](this.props, this.marker, e); 102 | } 103 | } 104 | } 105 | 106 | render() { 107 | return null; 108 | } 109 | } 110 | 111 | Marker.propTypes = { 112 | position: PropTypes.object, 113 | map: PropTypes.object 114 | } 115 | 116 | evtNames.forEach(e => Marker.propTypes[e] = PropTypes.func) 117 | 118 | Marker.defaultProps = { 119 | name: 'Marker' 120 | } 121 | 122 | export default Marker 123 | -------------------------------------------------------------------------------- /src/components/HeatMap.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { camelize } from '../lib/String' 5 | const evtNames = ['click', 'mouseover', 'recenter']; 6 | 7 | const wrappedPromise = function() { 8 | var wrappedPromise = {}, 9 | promise = new Promise(function (resolve, reject) { 10 | wrappedPromise.resolve = resolve; 11 | wrappedPromise.reject = reject; 12 | }); 13 | wrappedPromise.then = promise.then.bind(promise); 14 | wrappedPromise.catch = promise.catch.bind(promise); 15 | wrappedPromise.promise = promise; 16 | 17 | return wrappedPromise; 18 | } 19 | 20 | export class HeatMap extends React.Component { 21 | 22 | componentDidMount() { 23 | this.heatMapPromise = wrappedPromise(); 24 | this.renderHeatMap(); 25 | } 26 | 27 | componentDidUpdate(prevProps) { 28 | if ((this.props.map !== prevProps.map) || 29 | (this.props.position !== prevProps.position)) { 30 | if (this.heatMap) { 31 | this.heatMap.setMap(null); 32 | this.renderHeatMap(); 33 | } 34 | } 35 | } 36 | 37 | componentWillUnmount() { 38 | if (this.heatMap) { 39 | this.heatMap.setMap(null); 40 | } 41 | } 42 | 43 | renderHeatMap() { 44 | const { 45 | map, 46 | google, 47 | positions, 48 | mapCenter, 49 | icon, 50 | gradient, 51 | radius = 20, 52 | opacity = 0.2, 53 | ...props 54 | } = this.props; 55 | 56 | if (!google) { 57 | return null; 58 | } 59 | 60 | const data = positions.map((pos) => { 61 | return {location: new google.maps.LatLng(pos.lat, pos.lng), weight:pos.weight} 62 | }); 63 | 64 | const pref = { 65 | map, 66 | gradient, 67 | radius, 68 | opacity, 69 | data, 70 | ...props 71 | }; 72 | 73 | this.heatMap = new google.maps.visualization.HeatmapLayer(pref); 74 | 75 | this.heatMap.set('radius', radius === undefined ? 20 : radius); 76 | 77 | this.heatMap.set('opacity', opacity === undefined ? 0.2 : opacity); 78 | 79 | evtNames.forEach(e => { 80 | this.heatMap.addListener(e, this.handleEvent(e)); 81 | }); 82 | 83 | this.heatMapPromise.resolve(this.heatMap); 84 | } 85 | 86 | getHeatMap() { 87 | return this.heatMapPromise; 88 | } 89 | 90 | handleEvent(evt) { 91 | return (e) => { 92 | const evtName = `on${camelize(evt)}` 93 | if (this.props[evtName]) { 94 | this.props[evtName](this.props, this.heatMap, e); 95 | } 96 | } 97 | } 98 | 99 | render() { 100 | return null; 101 | } 102 | } 103 | 104 | HeatMap.propTypes = { 105 | position: PropTypes.object, 106 | map: PropTypes.object, 107 | icon: PropTypes.string 108 | } 109 | 110 | evtNames.forEach(e => HeatMap.propTypes[e] = PropTypes.func) 111 | 112 | HeatMap.defaultProps = { 113 | name: 'HeatMap' 114 | } 115 | 116 | export default HeatMap 117 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { 5 | Redirect, 6 | Switch, 7 | Link, 8 | Route, 9 | BrowserRouter as Router 10 | } from 'react-router-dom'; 11 | 12 | import Container from './Container'; 13 | 14 | import Simple from './components/basic'; 15 | import Marker from './components/withMarkers'; 16 | import ClickableMarkers from './components/clickableMarkers'; 17 | import GooglePlaces from './components/places'; 18 | import Autocomplete from './components/autocomplete'; 19 | import HeatMap from './components/withHeatMap'; 20 | import Polygon from './components/withPolygons'; 21 | import Polyline from './components/withPolylines'; 22 | import Rectangle from './components/withRectangle'; 23 | import CustomEvents from './components/resizeEvent'; 24 | 25 | const routes = [ 26 | { 27 | path: '/basic', 28 | name: 'Simple', 29 | component: Simple 30 | }, 31 | { 32 | path: '/markers', 33 | name: 'Marker', 34 | component: Marker 35 | }, 36 | { 37 | path: '/clickable_markers', 38 | name: 'Clickable markers', 39 | component: ClickableMarkers 40 | }, 41 | { 42 | path: '/places', 43 | name: 'Google places', 44 | component: GooglePlaces 45 | }, 46 | { 47 | path: '/autocomplete', 48 | name: 'Autocomplete', 49 | component: Autocomplete 50 | }, 51 | { 52 | path: '/heatMap', 53 | name: 'Heat Map', 54 | component: HeatMap 55 | }, 56 | { 57 | path: '/polygons', 58 | name: 'Polygon', 59 | component: Polygon 60 | }, 61 | { 62 | path: '/polyline', 63 | name: 'Polyline', 64 | component: Polyline 65 | }, 66 | { 67 | path: '/rectangle', 68 | name: 'Rectangle', 69 | component: Rectangle 70 | }, 71 | { 72 | path: '/onResizeEvent', 73 | name: 'Custom events', 74 | component: CustomEvents 75 | } 76 | ]; 77 | 78 | const createElement = (Component, route) => { 79 | // const pathname = props.location.pathname.replace('/', ''); 80 | // const routeDef = routes[pathname]; 81 | 82 | const newProps = { 83 | key: route.name, 84 | route, 85 | routes, 86 | // pathname, 87 | routeDef: route 88 | // routeDef 89 | }; 90 | 91 | return ; 92 | }; 93 | 94 | const Routing = ( 95 | 96 | 97 | 98 | ); 99 | 100 | // createElement(Container, routeProps)} path="/"> 101 | // {Object.keys(routes).map(key => { 102 | // const r = routes[key]; 103 | // })} 104 | // 105 | const mountNode = document.querySelector('#root'); 106 | 107 | if (mountNode) ReactDOM.render(Routing, mountNode); 108 | else { 109 | const hljs = require('highlight.js'); 110 | 111 | const codes = document.querySelectorAll('pre code'); 112 | 113 | for (let i = 0; i < codes.length; i += 1) { 114 | const block = codes[i]; 115 | hljs.highlightBlock(block); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/components/InfoWindow.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import ReactDOM from 'react-dom' 4 | import ReactDOMServer from 'react-dom/server' 5 | 6 | export class InfoWindow extends React.Component { 7 | 8 | componentDidMount() { 9 | this.renderInfoWindow(); 10 | } 11 | 12 | componentDidUpdate(prevProps) { 13 | const {google, map} = this.props; 14 | 15 | if (!google || !map) { 16 | return; 17 | } 18 | 19 | if (map !== prevProps.map) { 20 | this.renderInfoWindow(); 21 | } 22 | 23 | if (this.props.position !== prevProps.position) { 24 | this.updatePosition(); 25 | } 26 | 27 | if (this.props.children !== prevProps.children) { 28 | this.updateContent(); 29 | } 30 | 31 | if ((this.props.visible !== prevProps.visible || 32 | this.props.marker !== prevProps.marker || 33 | this.props.position !== prevProps.position)) { 34 | this.props.visible ? 35 | this.openWindow() : 36 | this.closeWindow(); 37 | } 38 | } 39 | 40 | renderInfoWindow() { 41 | const { 42 | map, 43 | google, 44 | mapCenter, 45 | ...props 46 | } = this.props; 47 | 48 | if (!google || !google.maps) { 49 | return; 50 | } 51 | 52 | const iw = this.infowindow = new google.maps.InfoWindow({ 53 | content: '', 54 | ...props 55 | }); 56 | 57 | google.maps.event 58 | .addListener(iw, 'closeclick', this.onClose.bind(this)) 59 | google.maps.event 60 | .addListener(iw, 'domready', this.onOpen.bind(this)); 61 | } 62 | 63 | onOpen() { 64 | if (this.props.onOpen) { 65 | this.props.onOpen(); 66 | } 67 | } 68 | 69 | onClose() { 70 | if (this.props.onClose) { 71 | this.props.onClose(); 72 | } 73 | } 74 | 75 | openWindow() { 76 | this.infowindow.open(this.props.map, this.props.marker); 77 | } 78 | 79 | updatePosition() { 80 | let pos = this.props.position; 81 | if (!(pos instanceof google.maps.LatLng)) { 82 | pos = pos && new google.maps.LatLng(pos.lat, pos.lng); 83 | } 84 | this.infowindow.setPosition(pos); 85 | } 86 | 87 | updateContent() { 88 | const content = this.renderChildren(); 89 | this.infowindow.setContent(content); 90 | } 91 | 92 | closeWindow() { 93 | this.infowindow.close(); 94 | } 95 | 96 | renderChildren() { 97 | const {children} = this.props; 98 | return ReactDOMServer.renderToString(children); 99 | } 100 | 101 | render() { 102 | return null; 103 | } 104 | } 105 | 106 | InfoWindow.propTypes = { 107 | children: PropTypes.element.isRequired, 108 | map: PropTypes.object, 109 | marker: PropTypes.object, 110 | position: PropTypes.object, 111 | visible: PropTypes.bool, 112 | 113 | // callbacks 114 | onClose: PropTypes.func, 115 | onOpen: PropTypes.func 116 | } 117 | 118 | InfoWindow.defaultProps = { 119 | visible: false 120 | } 121 | 122 | export default InfoWindow 123 | -------------------------------------------------------------------------------- /src/components/Rectangle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { areBoundsEqual } from '../lib/areBoundsEqual'; 5 | import { camelize } from '../lib/String'; 6 | const evtNames = ['click', 'mouseout', 'mouseover']; 7 | 8 | const wrappedPromise = function() { 9 | var wrappedPromise = {}, 10 | promise = new Promise(function (resolve, reject) { 11 | wrappedPromise.resolve = resolve; 12 | wrappedPromise.reject = reject; 13 | }); 14 | wrappedPromise.then = promise.then.bind(promise); 15 | wrappedPromise.catch = promise.catch.bind(promise); 16 | wrappedPromise.promise = promise; 17 | 18 | return wrappedPromise; 19 | } 20 | 21 | export class Rectangle extends React.Component { 22 | componentDidMount() { 23 | this.rectanglePromise = wrappedPromise(); 24 | this.renderRectangle(); 25 | } 26 | 27 | componentDidUpdate(prevProps) { 28 | if ( 29 | this.props.map !== prevProps.map || 30 | !areBoundsEqual(this.props.bounds, prevProps.bounds) 31 | ) { 32 | if (this.rectangle) { 33 | this.rectangle.setMap(null); 34 | } 35 | this.renderRectangle(); 36 | } 37 | } 38 | 39 | componentWillUnmount() { 40 | if (this.rectangle) { 41 | this.rectangle.setMap(null); 42 | } 43 | } 44 | 45 | renderRectangle() { 46 | const { 47 | map, 48 | google, 49 | bounds, 50 | strokeColor, 51 | strokeOpacity, 52 | strokeWeight, 53 | fillColor, 54 | fillOpacity, 55 | ...props 56 | } = this.props; 57 | 58 | if (!google) { 59 | return null; 60 | } 61 | 62 | const params = { 63 | map, 64 | bounds, 65 | strokeColor, 66 | strokeOpacity, 67 | strokeWeight, 68 | fillColor, 69 | fillOpacity, 70 | ...props 71 | }; 72 | 73 | this.rectangle = new google.maps.Rectangle(params); 74 | 75 | evtNames.forEach(e => { 76 | this.rectangle.addListener(e, this.handleEvent(e)); 77 | }); 78 | 79 | this.rectanglePromise.resolve(this.rectangle); 80 | } 81 | 82 | getRectangle() { 83 | return this.rectanglePromise; 84 | } 85 | 86 | handleEvent(evt) { 87 | return (e) => { 88 | const evtName = `on${camelize(evt)}` 89 | if (this.props[evtName]) { 90 | this.props[evtName](this.props, this.rectangle, e); 91 | } 92 | } 93 | } 94 | 95 | render() { 96 | console.log('hii, ', this.props.bounds); 97 | return null; 98 | } 99 | } 100 | 101 | Rectangle.propTypes = { 102 | bounds: PropTypes.object, 103 | strokeColor: PropTypes.string, 104 | strokeOpacity: PropTypes.number, 105 | strokeWeight: PropTypes.number, 106 | fillColor: PropTypes.string, 107 | fillOpacity: PropTypes.number 108 | } 109 | 110 | evtNames.forEach(e => Rectangle.propTypes[e] = PropTypes.func) 111 | 112 | Rectangle.defaultProps = { 113 | name: 'Rectangle' 114 | } 115 | 116 | export default Rectangle 117 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // require('babel-register'); 2 | 3 | const env = process.env; 4 | const NODE_ENV = process.env.NODE_ENV; 5 | const isDev = NODE_ENV === 'development'; 6 | const isTest = NODE_ENV === 'test'; 7 | 8 | const webpack = require('webpack'); 9 | const marked = require('marked'); 10 | const fs = require('fs'); 11 | const path = require('path'), 12 | join = path.join, 13 | resolve = path.resolve; 14 | 15 | const root = resolve(__dirname); 16 | const src = join(root, 'src'); 17 | const examples = join(root, 'examples'); 18 | const modules = join(root, 'node_modules'); 19 | const dest = join(root, 'public'); 20 | 21 | const getConfig = require('hjs-webpack') 22 | 23 | var config = getConfig({ 24 | isDev, 25 | in: join(examples, 'index.js'), 26 | out: dest, 27 | clearBeforeBuild: true, 28 | html: function(context, cb) { 29 | context.publicPath = isDev ? 'http://localhost:3000/' : '' 30 | 31 | fs.readFile(join(root, 'README.md'), (err, data) => { 32 | if (err) { 33 | return cb(err); 34 | } 35 | cb(null, { 36 | 'index.html': context.defaultTemplate(), 37 | // 'readme.html': context.defaultTemplate({ 38 | // html: `
39 | // ${marked(data.toString('utf-8'))} 40 | //
`, 41 | // metaTags: { 42 | // bootApp: false 43 | // } 44 | // }) 45 | }) 46 | }) 47 | } 48 | }); 49 | 50 | const dotenv = require('dotenv'); 51 | const envVariables = dotenv.config(); 52 | 53 | // Converts keys to be surrounded with __ 54 | const defines = 55 | Object.keys(envVariables) 56 | .reduce((memo, key) => { 57 | const val = JSON.stringify(envVariables[key]); 58 | memo[`__${key.toUpperCase()}__`] = val; 59 | return memo; 60 | }, { 61 | __NODE_ENV__: JSON.stringify(env.NODE_ENV), 62 | __IS_DEV__: isDev 63 | }) 64 | 65 | 66 | config.externals = { 67 | 'window.google': true 68 | } 69 | 70 | // Setup css modules require hook so it works when building for the server 71 | const cssModulesNames = `${isDev ? '[path][name]__[local]__' : ''}[hash:base64:5]`; 72 | const matchCssLoaders = /(^|!)(css-loader)($|!)/; 73 | 74 | const findLoader = (loaders, match, fn) => { 75 | const found = loaders.filter(l => l && l.loader && l.loader.match(match)) 76 | return found ? found[0] : null; 77 | } 78 | 79 | const cssloader = findLoader(config.module.loaders, matchCssLoaders); 80 | const newloader = Object.assign({}, cssloader, { 81 | test: /\.module\.css$/, 82 | include: [src, examples], 83 | loader: cssloader.loader.replace(matchCssLoaders, `$1$2?modules&localIdentName=${cssModulesNames}$3`) 84 | }) 85 | config.module.loaders.push(newloader); 86 | cssloader.test = new RegExp(`[^module]${cssloader.test.source}`) 87 | cssloader.loader = 'style!css!postcss' 88 | 89 | cssloader.include = [src, examples]; 90 | 91 | config.module.loaders.push({ 92 | test: /\.css$/, 93 | include: [modules], 94 | loader: 'style!css' 95 | }); 96 | 97 | 98 | config.plugins = [ 99 | new webpack.DefinePlugin(defines) 100 | ].concat(config.plugins); 101 | 102 | config.postcss = [].concat([ 103 | require('precss')({}), 104 | require('autoprefixer')({}), 105 | require('cssnano')({}) 106 | ]) 107 | 108 | module.exports = config; 109 | -------------------------------------------------------------------------------- /src/components/Circle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { arePathsEqual } from '../lib/arePathsEqual'; 5 | import { camelize } from '../lib/String'; 6 | const evtNames = ['click', 'mouseout', 'mouseover']; 7 | 8 | const wrappedPromise = function() { 9 | var wrappedPromise = {}, 10 | promise = new Promise(function 11 | (resolve, reject) { 12 | wrappedPromise.resolve = resolve; 13 | wrappedPromise.reject = reject; 14 | }); 15 | wrappedPromise.then = promise.then.bind(promise); 16 | wrappedPromise.catch = promise.catch.bind(promise); 17 | wrappedPromise.promise = promise; 18 | 19 | return wrappedPromise; 20 | } 21 | 22 | export class Circle extends React.Component { 23 | componentDidMount() { 24 | this.circlePromise = wrappedPromise(); 25 | this.renderCircle(); 26 | } 27 | 28 | componentDidUpdate(prevProps) { 29 | const { path, map } = this.props; 30 | 31 | if ( 32 | this.propsChanged(prevProps) || 33 | map !== prevProps.map || 34 | !arePathsEqual(path, prevProps.path) 35 | ) { 36 | this.destroyCircle(); 37 | this.renderCircle(); 38 | } 39 | } 40 | 41 | centerChanged = (newCenter) => { 42 | const { lat, lng } = this.props.center; 43 | return lat !== newCenter.lat || lng !== newCenter.lng; 44 | }; 45 | 46 | propsChanged = (newProps) => { 47 | if (this.centerChanged(newProps.center)) return true; 48 | 49 | return Object.keys(Circle.propTypes).some(key => ( 50 | this.props[key] !== newProps[key] 51 | )); 52 | }; 53 | 54 | componentWillUnmount() { 55 | this.destroyCircle(); 56 | } 57 | 58 | destroyCircle = () => { 59 | if (this.circle) { 60 | this.circle.setMap(null); 61 | } 62 | } 63 | 64 | renderCircle() { 65 | const { 66 | map, 67 | google, 68 | center, 69 | radius, 70 | strokeColor, 71 | strokeOpacity, 72 | strokeWeight, 73 | fillColor, 74 | fillOpacity, 75 | draggable, 76 | visible, 77 | ...props 78 | } = this.props; 79 | 80 | if (!google) { 81 | return null; 82 | } 83 | 84 | const params = { 85 | ...props, 86 | map, 87 | center, 88 | radius, 89 | draggable, 90 | visible, 91 | options: { 92 | strokeColor, 93 | strokeOpacity, 94 | strokeWeight, 95 | fillColor, 96 | fillOpacity, 97 | }, 98 | }; 99 | 100 | this.circle = new google.maps.Circle(params); 101 | 102 | evtNames.forEach(e => { 103 | this.circle.addListener(e, this.handleEvent(e)); 104 | }); 105 | 106 | this.circlePromise.resolve(this.circle); 107 | } 108 | 109 | getCircle() { 110 | return this.circlePromise; 111 | } 112 | 113 | handleEvent(evt) { 114 | return (e) => { 115 | const evtName = `on${camelize(evt)}` 116 | if (this.props[evtName]) { 117 | this.props[evtName](this.props, this.circle, e); 118 | } 119 | } 120 | } 121 | 122 | render() { 123 | return null; 124 | } 125 | } 126 | 127 | Circle.propTypes = { 128 | center: PropTypes.object, 129 | radius: PropTypes.number, 130 | strokeColor: PropTypes.string, 131 | strokeOpacity: PropTypes.number, 132 | strokeWeight: PropTypes.number, 133 | fillColor: PropTypes.string, 134 | fillOpacity: PropTypes.number, 135 | draggable: PropTypes.bool, 136 | visible: PropTypes.bool, 137 | } 138 | 139 | evtNames.forEach(e => Circle.propTypes[e] = PropTypes.func) 140 | 141 | Circle.defaultProps = { 142 | name: 'Circle' 143 | } 144 | 145 | export default Circle 146 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import 'googlemaps' 2 | import * as React from 'react' 3 | 4 | interface IGoogleApiOptions { 5 | apiKey: string, 6 | libraries?: string[], 7 | client?: string, 8 | url?: string, 9 | version?: string, 10 | language?: string, 11 | region?: string, 12 | LoadingContainer?: any 13 | } 14 | type GoogleApiOptionsFunc = (props: any) => IGoogleApiOptions 15 | 16 | type Omit = Pick> 17 | 18 | export type GoogleAPI = typeof google 19 | export function GoogleApiWrapper(opts: IGoogleApiOptions | GoogleApiOptionsFunc): 20 | (ctor: React.ComponentType) => React.ComponentType> 21 | 22 | export interface IProvidedProps { 23 | google: GoogleAPI 24 | loaded?: boolean 25 | } 26 | 27 | type mapEventHandler = (mapProps?: IMapProps, map?: google.maps.Map, event?: any) => any 28 | 29 | type Style = Object 30 | 31 | export interface IMapProps extends google.maps.MapOptions { 32 | google: GoogleAPI 33 | loaded?: boolean 34 | 35 | style?: Style 36 | containerStyle?: Style 37 | 38 | bounds?: google.maps.LatLngBounds | google.maps.LatLngBoundsLiteral 39 | centerAroundCurrentLocation?: boolean 40 | initialCenter?: google.maps.LatLngLiteral 41 | center?: google.maps.LatLngLiteral 42 | zoom?: number 43 | 44 | zoomControl?: boolean 45 | mapTypeControl?: boolean 46 | scaleControl?: boolean 47 | streetViewControl?: boolean 48 | panControl?: boolean 49 | rotateControl?: boolean 50 | fullscreenControl?: boolean 51 | 52 | visible?: boolean 53 | 54 | onReady?: mapEventHandler 55 | onClick?: mapEventHandler 56 | onDragend?: mapEventHandler 57 | onRecenter?: mapEventHandler 58 | onBoundsChanged?: mapEventHandler 59 | onCenterChanged?: mapEventHandler 60 | onDblclick?: mapEventHandler 61 | onDragstart?: mapEventHandler 62 | onHeadingChange?: mapEventHandler 63 | onIdle?: mapEventHandler 64 | onMaptypeidChanged?: mapEventHandler 65 | onMousemove?: mapEventHandler 66 | onMouseover?: mapEventHandler 67 | onMouseout?: mapEventHandler 68 | onProjectionChanged?: mapEventHandler 69 | onResize?: mapEventHandler 70 | onRightclick?: mapEventHandler 71 | onTilesloaded?: mapEventHandler 72 | onTiltChanged?: mapEventHandler 73 | onZoomChanged?: mapEventHandler 74 | } 75 | 76 | type markerEventHandler = (props?: IMarkerProps, marker?: google.maps.Marker, event?: any) => any 77 | 78 | export interface IMarkerProps extends Partial { 79 | mapCenter?: google.maps.LatLng | google.maps.LatLngLiteral 80 | position?: google.maps.LatLngLiteral 81 | label?: string 82 | title?: string 83 | name?: string 84 | 85 | onClick?: markerEventHandler 86 | onDblclick?: markerEventHandler 87 | onDragend?: markerEventHandler 88 | onMousedown?: markerEventHandler 89 | onMouseout?: markerEventHandler 90 | onMouseover?: markerEventHandler 91 | onDragend?: markerEventHandler 92 | onMouseup?: markerEventHandler 93 | onRecenter?: markerEventHandler 94 | } 95 | 96 | export class Map extends React.Component { 97 | 98 | } 99 | 100 | export class Marker extends React.Component { 101 | 102 | } 103 | 104 | export class Polygon extends React.Component { 105 | 106 | } 107 | 108 | export class Polyline extends React.Component { 109 | 110 | } 111 | 112 | export class Circle extends React.Component { 113 | 114 | } 115 | 116 | export interface IInfoWindowProps extends Partial { 117 | 118 | google?: typeof google 119 | map?: google.maps.Map 120 | marker?: google.maps.Marker 121 | 122 | position?: google.maps.LatLng | google.maps.LatLngLiteral 123 | visible?: boolean 124 | 125 | children: React.ReactNode 126 | onClose?(): void 127 | onOpen?(): void 128 | 129 | } 130 | 131 | export class InfoWindow extends React.Component { 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/GoogleApiComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import {ScriptCache} from './lib/ScriptCache'; 5 | import GoogleApi from './lib/GoogleApi'; 6 | 7 | const defaultMapConfig = {}; 8 | 9 | const serialize = obj => JSON.stringify(obj); 10 | const isSame = (obj1, obj2) => obj1 === obj2 || serialize(obj1) === serialize(obj2); 11 | 12 | const defaultCreateCache = options => { 13 | options = options || {}; 14 | const apiKey = options.apiKey; 15 | const libraries = options.libraries || ['places']; 16 | const version = options.version || '3'; 17 | const language = options.language || 'en'; 18 | const url = options.url; 19 | const client = options.client; 20 | const region = options.region; 21 | 22 | return ScriptCache({ 23 | google: GoogleApi({ 24 | apiKey: apiKey, 25 | language: language, 26 | libraries: libraries, 27 | version: version, 28 | url: url, 29 | client: client, 30 | region: region 31 | }) 32 | }); 33 | }; 34 | 35 | const DefaultLoadingContainer = props =>
Loading...
; 36 | 37 | export const wrapper = (input, className, style) => WrappedComponent => { 38 | class Wrapper extends React.Component { 39 | constructor(props, context) { 40 | super(props, context); 41 | 42 | // Build options from input 43 | const options = typeof input === 'function' ? input(props) : input; 44 | 45 | // Initialize required Google scripts and other configured options 46 | this.initialize(options); 47 | 48 | this.state = { 49 | loaded: false, 50 | map: null, 51 | google: null, 52 | options: options 53 | }; 54 | 55 | this.mapRef=React.createRef(); 56 | } 57 | 58 | componentDidUpdate(props) { 59 | // Do not update input if it's not dynamic 60 | if (typeof input !== 'function') { 61 | return; 62 | } 63 | 64 | // Get options to compare 65 | const prevOptions = this.state.options; 66 | const options = typeof input === 'function' ? input(props) : input; 67 | 68 | // Ignore when options are not changed 69 | if (isSame(options, prevOptions)) { 70 | return; 71 | } 72 | 73 | // Initialize with new options 74 | this.initialize(options); 75 | 76 | // Save new options in component state, 77 | // and remove information about previous API handlers 78 | this.state= { 79 | options: options, 80 | loaded: false, 81 | google: null 82 | }; 83 | } 84 | 85 | componentWillUnmount() { 86 | if (this.unregisterLoadHandler) { 87 | this.unregisterLoadHandler(); 88 | } 89 | } 90 | 91 | initialize(options) { 92 | // Avoid race condition: remove previous 'load' listener 93 | if (this.unregisterLoadHandler) { 94 | this.unregisterLoadHandler(); 95 | this.unregisterLoadHandler = null; 96 | } 97 | 98 | // Load cache factory 99 | const createCache = options.createCache || defaultCreateCache; 100 | 101 | // Build script 102 | this.scriptCache = createCache(options); 103 | this.unregisterLoadHandler = 104 | this.scriptCache.google.onLoad(this.onLoad.bind(this)); 105 | 106 | // Store information about loading container 107 | this.LoadingContainer = 108 | options.LoadingContainer || DefaultLoadingContainer; 109 | } 110 | 111 | onLoad(err, tag) { 112 | this._gapi = window.google; 113 | 114 | this.setState({loaded: true, google: this._gapi}); 115 | } 116 | 117 | render() { 118 | const {LoadingContainer} = this; 119 | if (!this.state.loaded) { 120 | return ; 121 | } 122 | 123 | const props = Object.assign({}, this.props, { 124 | loaded: this.state.loaded, 125 | google: window.google 126 | }); 127 | 128 | return ( 129 |
130 | 131 |
132 |
133 | ); 134 | } 135 | } 136 | 137 | return Wrapper; 138 | }; 139 | 140 | export default wrapper; 141 | -------------------------------------------------------------------------------- /src/__tests__/lib/arePathsEqual.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {expect} from 'chai'; 3 | import { arePathsEqual } from '../../lib/arePathsEqual'; 4 | 5 | describe('arePathsEqual', () => { 6 | it('considers null arrays equal', () => { 7 | expect(arePathsEqual(null, null)).to.equal(true); 8 | }); 9 | it('considers undefined equal', () => { 10 | expect(arePathsEqual(undefined, undefined)).to.equal(true); 11 | }); 12 | it('considers empty arrays equal', () => { 13 | expect(arePathsEqual([], [])).to.equal(true); 14 | }); 15 | it('considers paths unequal when one is null', () => { 16 | const mockPath = [ 17 | {lat: 37.78, lng: -122.45}, 18 | {lat: 37.69, lng: -122.49}, 19 | {lat: 37.22, lng: -122.33} 20 | ]; 21 | expect(arePathsEqual(mockPath, null)).to.equal(false); 22 | }); 23 | it('considers paths unequal if one is undefined', () => { 24 | const mockPath = [ 25 | {lat: 37.78, lng: -122.45}, 26 | {lat: 37.69, lng: -122.49}, 27 | {lat: 37.22, lng: -122.33} 28 | ]; 29 | expect(arePathsEqual(undefined, mockPath)).to.equal(false); 30 | }); 31 | it('dislikes invalid paths', () => { 32 | expect(arePathsEqual( 33 | [{lat: 100, long: 10}, {a: 1, b: 2}], [{a: 1, b: 2}, {a: 3, b: 4}] 34 | )).to.equal(false); 35 | }); 36 | it('correctly compares a path to self', () => { 37 | const mockPath = [ 38 | {lat: 37.78, lng: -122.45}, 39 | {lat: 37.69, lng: -122.49}, 40 | {lat: 37.22, lng: -122.33} 41 | ]; 42 | expect(arePathsEqual(mockPath, mockPath)).to.equal(true); 43 | }); 44 | it('requires vertices to be in the same order', () => { 45 | const mockPath = [ 46 | {lat: 37.78, lng: -122.45}, 47 | {lat: 37.69, lng: -122.49}, 48 | {lat: 37.22, lng: -122.33} 49 | ]; 50 | const mockPathSameOrder = [ 51 | {lat: 37.78, lng: -122.45}, 52 | {lat: 37.69, lng: -122.49}, 53 | {lat: 37.22, lng: -122.33} 54 | ]; 55 | expect(arePathsEqual(mockPath, mockPathSameOrder)).to.equal(true); 56 | }); 57 | it('considers paths unequal if vertices not in the same order', () => { 58 | const mockPath = [ 59 | {lat: 37.78, lng: -122.45}, 60 | {lat: 37.69, lng: -122.49}, 61 | {lat: 37.22, lng: -122.33} 62 | ]; 63 | const mockPathChangedOrder = [ 64 | {lat: 37.22, lng: -122.33}, 65 | {lat: 37.78, lng: -122.45}, 66 | {lat: 37.69, lng: -122.49} 67 | ]; 68 | expect(arePathsEqual(mockPath, mockPathChangedOrder)).to.equal(false); 69 | }); 70 | it('requires paths to have equal lengths', () => { 71 | const mockPathLength3 = [ 72 | {lat: 37.78, lng: -122.45}, 73 | {lat: 37.69, lng: -122.49}, 74 | {lat: 37.22, lng: -122.33} 75 | ]; 76 | const mockPathLength5 = [ 77 | {lat: 37.78, lng: -122.45}, 78 | {lat: 37.69, lng: -122.49}, 79 | {lat: 37.22, lng: -122.33}, 80 | {lat: 37.54, lng: -122.13}, 81 | {lat: 37.11, lng: 125.18} 82 | ]; 83 | expect(arePathsEqual(mockPathLength3, mockPathLength5)).to.equal(false); 84 | }); 85 | it('detects differring lng values', () => { 86 | const mockPath = [ 87 | {lat: 37.78, lng: -122.45}, 88 | {lat: 37.69, lng: -122.49}, 89 | {lat: 37.22, lng: -122.33}, 90 | {lat: 37.54, lng: -122.13}, 91 | {lat: 37.11, lng: 125.18} 92 | ]; 93 | const mockPathLatDifferent = [ 94 | {lat: 37.78, lng: -122.45}, 95 | {lat: 37.69, lng: -122.49}, 96 | {lat: 37.22, lng: -122.33}, 97 | {lat: 37.54, lng: -122.13}, 98 | {lat: 37.76, lng: 125.18} 99 | ]; 100 | expect(arePathsEqual(mockPath, mockPathLatDifferent)).to.equal(false); 101 | }); 102 | it('detects differring lng values', () => { 103 | const mockPath = [ 104 | {lat: 37.78, lng: -122.45}, 105 | {lat: 37.69, lng: -122.49}, 106 | {lat: 37.22, lng: -122.33}, 107 | {lat: 37.54, lng: -122.13}, 108 | {lat: 37.11, lng: 125.18} 109 | ]; 110 | const mockPathLngDifferent = [ 111 | {lat: 37.78, lng: -122.45}, 112 | {lat: 37.69, lng: -122.49}, 113 | {lat: 37.22, lng: -122.899}, 114 | {lat: 37.54, lng: -122.13}, 115 | {lat: 37.11, lng: 125.18} 116 | ]; 117 | expect(arePathsEqual(mockPath, mockPathLngDifferent)).to.equal(false); 118 | }); 119 | it('detects differring vertices', () => { 120 | const mockPath = [ 121 | {lat: 37.78, lng: -122.45}, 122 | {lat: 37.69, lng: -122.49}, 123 | {lat: 37.22, lng: -122.899}, 124 | {lat: 37.54, lng: -122.13}, 125 | {lat: 37.11, lng: 125.18} 126 | ]; 127 | const anEntirelyDifferentMockPath = [ 128 | {lat: 37.70, lng: -122.413}, 129 | {lat: 37.70, lng: -122.412}, 130 | {lat: 37.81, lng: -122.89}, 131 | {lat: 39.56, lng: -122.41}, 132 | {lat: 42.76, lng: -126.15} 133 | ]; 134 | expect(arePathsEqual(mockPath, anEntirelyDifferentMockPath)).to.equal(false); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /src/lib/ScriptCache.js: -------------------------------------------------------------------------------- 1 | let counter = 0; 2 | let scriptMap = typeof window !== 'undefined' && window._scriptMap || new Map(); 3 | const window = require('./windowOrGlobal'); 4 | 5 | export const ScriptCache = (function(global) { 6 | global._scriptMap = global._scriptMap || scriptMap; 7 | return function ScriptCache(scripts) { 8 | const Cache = {}; 9 | 10 | Cache._onLoad = function(key) { 11 | return (cb) => { 12 | let registered = true; 13 | 14 | function unregister() { 15 | registered = false; 16 | } 17 | 18 | let stored = scriptMap.get(key); 19 | 20 | if (stored) { 21 | stored.promise.then(() => { 22 | if (registered) { 23 | stored.error ? cb(stored.error) : cb(null, stored) 24 | } 25 | 26 | return stored; 27 | }).catch(error => cb(error)); 28 | } else { 29 | // TODO: 30 | } 31 | 32 | return unregister; 33 | } 34 | }; 35 | 36 | Cache._scriptTag = (key, src) => { 37 | if (!scriptMap.has(key)) { 38 | // Server side rendering environments don't always have access to the `document` global. 39 | // In these cases, we're not going to be able to return a script tag, so just return null. 40 | if (typeof document === 'undefined') return null; 41 | 42 | let tag = document.createElement('script'); 43 | let promise = new Promise((resolve, reject) => { 44 | let body = document.getElementsByTagName('body')[0]; 45 | 46 | tag.type = 'text/javascript'; 47 | tag.async = false; // Load in order 48 | 49 | const cbName = `loaderCB${counter++}${Date.now()}`; 50 | let cb; 51 | 52 | let handleResult = (state) => { 53 | return (evt) => { 54 | let stored = scriptMap.get(key); 55 | if (state === 'loaded') { 56 | stored.resolved = true; 57 | resolve(src); 58 | // stored.handlers.forEach(h => h.call(null, stored)) 59 | // stored.handlers = [] 60 | } else if (state === 'error') { 61 | stored.errored = true; 62 | // stored.handlers.forEach(h => h.call(null, stored)) 63 | // stored.handlers = []; 64 | reject(evt) 65 | } 66 | stored.loaded = true; 67 | 68 | cleanup(); 69 | } 70 | }; 71 | 72 | const cleanup = () => { 73 | if (global[cbName] && typeof global[cbName] === 'function') { 74 | global[cbName] = null; 75 | delete global[cbName] 76 | } 77 | }; 78 | 79 | tag.onload = handleResult('loaded'); 80 | tag.onerror = handleResult('error'); 81 | tag.onreadystatechange = () => { 82 | handleResult(tag.readyState) 83 | }; 84 | 85 | // Pick off callback, if there is one 86 | if (src.match(/callback=CALLBACK_NAME/)) { 87 | src = src.replace(/(callback=)[^\&]+/, `$1${cbName}`); 88 | cb = window[cbName] = tag.onload; 89 | } else { 90 | tag.addEventListener('load', tag.onload) 91 | } 92 | tag.addEventListener('error', tag.onerror); 93 | 94 | tag.src = src; 95 | body.appendChild(tag); 96 | 97 | return tag; 98 | }); 99 | let initialState = { 100 | loaded: false, 101 | error: false, 102 | promise, 103 | tag 104 | }; 105 | scriptMap.set(key, initialState); 106 | } 107 | return scriptMap.get(key).tag; 108 | }; 109 | 110 | // let scriptTags = document.querySelectorAll('script') 111 | // 112 | // NodeList.prototype.filter = Array.prototype.filter; 113 | // NodeList.prototype.map = Array.prototype.map; 114 | // const initialScripts = scriptTags 115 | // .filter(s => !!s.src) 116 | // .map(s => s.src.split('?')[0]) 117 | // .reduce((memo, script) => { 118 | // memo[script] = script; 119 | // return memo; 120 | // }, {}); 121 | 122 | Object.keys(scripts).forEach(function(key) { 123 | const script = scripts[key]; 124 | 125 | const tag = window._scriptMap.has(key) ? 126 | window._scriptMap.get(key).tag : 127 | Cache._scriptTag(key, script); 128 | 129 | Cache[key] = { 130 | tag: tag, 131 | onLoad: Cache._onLoad(key), 132 | } 133 | }); 134 | 135 | return Cache; 136 | } 137 | })(window); 138 | 139 | export default ScriptCache; 140 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ReactDOM from 'react-dom'; 4 | import {camelize} from './lib/String'; 5 | import {makeCancelable} from './lib/cancelablePromise'; 6 | 7 | const mapStyles = { 8 | container: { 9 | position: 'absolute', 10 | width: '100%', 11 | height: '100%' 12 | }, 13 | map: { 14 | position: 'absolute', 15 | left: 0, 16 | right: 0, 17 | bottom: 0, 18 | top: 0 19 | } 20 | }; 21 | 22 | const evtNames = [ 23 | 'ready', 24 | 'click', 25 | 'dragend', 26 | 'recenter', 27 | 'bounds_changed', 28 | 'center_changed', 29 | 'dblclick', 30 | 'dragstart', 31 | 'heading_change', 32 | 'idle', 33 | 'maptypeid_changed', 34 | 'mousemove', 35 | 'mouseout', 36 | 'mouseover', 37 | 'projection_changed', 38 | 'resize', 39 | 'rightclick', 40 | 'tilesloaded', 41 | 'tilt_changed', 42 | 'zoom_changed' 43 | ]; 44 | 45 | export {wrapper as GoogleApiWrapper} from './GoogleApiComponent'; 46 | export {Marker} from './components/Marker'; 47 | export {InfoWindow} from './components/InfoWindow'; 48 | export {HeatMap} from './components/HeatMap'; 49 | export {Polygon} from './components/Polygon'; 50 | export {Polyline} from './components/Polyline'; 51 | export {Circle} from './components/Circle'; 52 | export {Rectangle} from './components/Rectangle'; 53 | 54 | export class Map extends React.Component { 55 | constructor(props) { 56 | super(props); 57 | 58 | if (!props.hasOwnProperty('google')) { 59 | throw new Error('You must include a `google` prop'); 60 | } 61 | 62 | this.listeners = {}; 63 | this.state = { 64 | currentLocation: { 65 | lat: this.props.initialCenter.lat, 66 | lng: this.props.initialCenter.lng 67 | } 68 | }; 69 | 70 | this.mapRef=React.createRef(); 71 | } 72 | 73 | componentDidMount() { 74 | if (this.props.centerAroundCurrentLocation) { 75 | if (navigator && navigator.geolocation) { 76 | this.geoPromise = makeCancelable( 77 | new Promise((resolve, reject) => { 78 | navigator.geolocation.getCurrentPosition(resolve, reject); 79 | }) 80 | ); 81 | 82 | this.geoPromise.promise 83 | .then(pos => { 84 | const coords = pos.coords; 85 | this.setState({ 86 | currentLocation: { 87 | lat: coords.latitude, 88 | lng: coords.longitude 89 | } 90 | }); 91 | }) 92 | .catch(e => e); 93 | } 94 | } 95 | this.loadMap(); 96 | } 97 | 98 | componentDidUpdate(prevProps, prevState) { 99 | if (prevProps.google !== this.props.google) { 100 | this.loadMap(); 101 | } 102 | if (this.props.visible !== prevProps.visible) { 103 | this.restyleMap(); 104 | } 105 | if (this.props.zoom !== prevProps.zoom) { 106 | this.map.setZoom(this.props.zoom); 107 | } 108 | if (this.props.center !== prevProps.center) { 109 | this.setState({ 110 | currentLocation: this.props.center 111 | }); 112 | } 113 | if (prevState.currentLocation !== this.state.currentLocation) { 114 | this.recenterMap(); 115 | } 116 | if (this.props.bounds && this.props.bounds !== prevProps.bounds) { 117 | this.map.fitBounds(this.props.bounds); 118 | } 119 | } 120 | 121 | componentWillUnmount() { 122 | const {google} = this.props; 123 | if (this.geoPromise) { 124 | this.geoPromise.cancel(); 125 | } 126 | Object.keys(this.listeners).forEach(e => { 127 | google.maps.event.removeListener(this.listeners[e]); 128 | }); 129 | } 130 | 131 | loadMap() { 132 | if (this.props && this.props.google) { 133 | const {google} = this.props; 134 | const maps = google.maps; 135 | 136 | const mapRef = this.mapRef.current; 137 | const node = ReactDOM.findDOMNode(mapRef); 138 | const curr = this.state.currentLocation; 139 | const center = new maps.LatLng(curr.lat, curr.lng); 140 | 141 | const mapTypeIds = this.props.google.maps.MapTypeId || {}; 142 | const mapTypeFromProps = String(this.props.mapType).toUpperCase(); 143 | 144 | const mapConfig = Object.assign( 145 | {}, 146 | { 147 | mapTypeId: mapTypeIds[mapTypeFromProps], 148 | center: center, 149 | zoom: this.props.zoom, 150 | maxZoom: this.props.maxZoom, 151 | minZoom: this.props.minZoom, 152 | clickableIcons: !!this.props.clickableIcons, 153 | disableDefaultUI: this.props.disableDefaultUI, 154 | zoomControl: this.props.zoomControl, 155 | zoomControlOptions: this.props.zoomControlOptions, 156 | mapTypeControl: this.props.mapTypeControl, 157 | mapTypeControlOptions: this.props.mapTypeControlOptions, 158 | scaleControl: this.props.scaleControl, 159 | streetViewControl: this.props.streetViewControl, 160 | streetViewControlOptions: this.props.streetViewControlOptions, 161 | panControl: this.props.panControl, 162 | rotateControl: this.props.rotateControl, 163 | fullscreenControl: this.props.fullscreenControl, 164 | scrollwheel: this.props.scrollwheel, 165 | draggable: this.props.draggable, 166 | draggableCursor: this.props.draggableCursor, 167 | keyboardShortcuts: this.props.keyboardShortcuts, 168 | disableDoubleClickZoom: this.props.disableDoubleClickZoom, 169 | noClear: this.props.noClear, 170 | styles: this.props.styles, 171 | gestureHandling: this.props.gestureHandling 172 | } 173 | ); 174 | 175 | Object.keys(mapConfig).forEach(key => { 176 | // Allow to configure mapConfig with 'false' 177 | if (mapConfig[key] === null) { 178 | delete mapConfig[key]; 179 | } 180 | }); 181 | 182 | this.map = new maps.Map(node, mapConfig); 183 | 184 | evtNames.forEach(e => { 185 | this.listeners[e] = this.map.addListener(e, this.handleEvent(e)); 186 | }); 187 | maps.event.trigger(this.map, 'ready'); 188 | this.forceUpdate(); 189 | } 190 | } 191 | 192 | handleEvent(evtName) { 193 | let timeout; 194 | const handlerName = `on${camelize(evtName)}`; 195 | 196 | return e => { 197 | if (timeout) { 198 | clearTimeout(timeout); 199 | timeout = null; 200 | } 201 | timeout = setTimeout(() => { 202 | if (this.props[handlerName]) { 203 | this.props[handlerName](this.props, this.map, e); 204 | } 205 | }, 0); 206 | }; 207 | } 208 | 209 | recenterMap() { 210 | const map = this.map; 211 | 212 | const {google} = this.props; 213 | 214 | if (!google) return; 215 | const maps = google.maps; 216 | 217 | if (map) { 218 | let center = this.state.currentLocation; 219 | if (!(center instanceof google.maps.LatLng)) { 220 | center = new google.maps.LatLng(center.lat, center.lng); 221 | } 222 | // map.panTo(center) 223 | map.setCenter(center); 224 | maps.event.trigger(map, 'recenter'); 225 | } 226 | } 227 | 228 | restyleMap() { 229 | if (this.map) { 230 | const {google} = this.props; 231 | google.maps.event.trigger(this.map, 'resize'); 232 | } 233 | } 234 | 235 | renderChildren() { 236 | const {children} = this.props; 237 | 238 | if (!children) return; 239 | 240 | return React.Children.map(children, c => { 241 | if (!c) return; 242 | return React.cloneElement(c, { 243 | map: this.map, 244 | google: this.props.google, 245 | mapCenter: this.state.currentLocation 246 | }); 247 | }); 248 | } 249 | 250 | render() { 251 | const style = Object.assign({}, mapStyles.map, this.props.style, { 252 | display: this.props.visible ? 'inherit' : 'none' 253 | }); 254 | 255 | const containerStyles = Object.assign( 256 | {}, 257 | mapStyles.container, 258 | this.props.containerStyle 259 | ); 260 | 261 | return ( 262 |
263 |
264 | Loading map... 265 |
266 | {this.renderChildren()} 267 |
268 | ); 269 | } 270 | } 271 | 272 | Map.propTypes = { 273 | google: PropTypes.object, 274 | zoom: PropTypes.number, 275 | centerAroundCurrentLocation: PropTypes.bool, 276 | center: PropTypes.object, 277 | initialCenter: PropTypes.object, 278 | className: PropTypes.string, 279 | style: PropTypes.object, 280 | containerStyle: PropTypes.object, 281 | visible: PropTypes.bool, 282 | mapType: PropTypes.string, 283 | maxZoom: PropTypes.number, 284 | minZoom: PropTypes.number, 285 | clickableIcons: PropTypes.bool, 286 | disableDefaultUI: PropTypes.bool, 287 | zoomControl: PropTypes.bool, 288 | zoomControlOptions: PropTypes.object, 289 | mapTypeControl: PropTypes.bool, 290 | mapTypeControlOptions: PropTypes.bool, 291 | scaleControl: PropTypes.bool, 292 | streetViewControl: PropTypes.bool, 293 | streetViewControlOptions: PropTypes.object, 294 | panControl: PropTypes.bool, 295 | rotateControl: PropTypes.bool, 296 | fullscreenControl: PropTypes.bool, 297 | scrollwheel: PropTypes.bool, 298 | draggable: PropTypes.bool, 299 | draggableCursor: PropTypes.string, 300 | keyboardShortcuts: PropTypes.bool, 301 | disableDoubleClickZoom: PropTypes.bool, 302 | noClear: PropTypes.bool, 303 | styles: PropTypes.array, 304 | gestureHandling: PropTypes.string, 305 | bounds: PropTypes.object 306 | }; 307 | 308 | evtNames.forEach(e => (Map.propTypes[camelize(e)] = PropTypes.func)); 309 | 310 | Map.defaultProps = { 311 | zoom: 14, 312 | initialCenter: { 313 | lat: 37.774929, 314 | lng: -122.419416 315 | }, 316 | center: {}, 317 | centerAroundCurrentLocation: false, 318 | style: {}, 319 | containerStyle: {}, 320 | visible: true 321 | }; 322 | 323 | export default Map; 324 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Fullstack React Google Maps Tutorial 3 |

4 | 5 | # Google Map React Component Tutorial [![Dolpins](https://cdn.rawgit.com/fullstackreact/google-maps-react/master/resources/readme/dolphins-badge-ff00ff.svg)](https://www.fullstackreact.com) 6 | 7 | > A declarative Google Map React component using React, lazy-loading dependencies, current-location finder and a test-driven approach by the [Fullstack React](https://fullstackreact.com) team. 8 | 9 | See the [demo](https://fullstackreact.github.io/google-maps-react) and [accompanying blog post](https://www.fullstackreact.com/articles/how-to-write-a-google-maps-react-component/). 10 | 11 | ## Quickstart 12 | 13 | First, install the library: 14 | 15 | ```shell 16 | npm install --save google-maps-react 17 | ``` 18 | ## Automatically Lazy-loading Google API 19 | 20 | The library includes a helper to wrap around the Google maps API. The `GoogleApiWrapper` Higher-Order component accepts a configuration object which *must* include an `apiKey`. See [lib/GoogleApi.js](https://github.com/fullstackreact/google-maps-react/blob/master/src/lib/GoogleApi.js#L4) for all options it accepts. 21 | 22 | ```javascript 23 | import {GoogleApiWrapper} from 'google-maps-react'; 24 | 25 | // ... 26 | 27 | export class MapContainer extends React.Component {} 28 | 29 | export default GoogleApiWrapper({ 30 | apiKey: (YOUR_GOOGLE_API_KEY_GOES_HERE) 31 | })(MapContainer) 32 | ``` 33 | 34 | Alternatively, the `GoogleApiWrapper` Higher-Order component can be configured by passing a function that will be called with whe wrapped component's `props` and should returned the configuration object. 35 | 36 | ```javascript 37 | export default GoogleApiWrapper( 38 | (props) => ({ 39 | apiKey: props.apiKey, 40 | language: props.language, 41 | } 42 | ))(MapContainer) 43 | ``` 44 | 45 | If you want to add a loading container _other than the default_ loading container, simply pass it in the HOC, like so: 46 | 47 | ```javascript 48 | const LoadingContainer = (props) => ( 49 |
Fancy loading container!
50 | ) 51 | 52 | export default GoogleApiWrapper({ 53 | apiKey: (YOUR_GOOGLE_API_KEY_GOES_HERE), 54 | LoadingContainer: LoadingContainer 55 | })(MapContainer) 56 | ``` 57 | 58 | ## Sample Usage With Lazy-loading Google API: 59 | 60 | ```javascript 61 | import {Map, InfoWindow, Marker, GoogleApiWrapper} from 'google-maps-react'; 62 | 63 | export class MapContainer extends Component { 64 | render() { 65 | return ( 66 | 67 | 68 | 70 | 71 | 72 |
73 |

{this.state.selectedPlace.name}

74 |
75 |
76 |
77 | ); 78 | } 79 | } 80 | 81 | export default GoogleApiWrapper({ 82 | apiKey: (YOUR_GOOGLE_API_KEY_GOES_HERE) 83 | })(MapContainer) 84 | ``` 85 | *Note: [Marker](#marker) and [InfoWindow](#infowindow--sample-event-handler-functions) components are disscussed below.* 86 | 87 | ![](http://d.pr/i/C7qr.png) 88 | 89 | ## Examples 90 | 91 | Check out the example site at: [http://fullstackreact.github.io/google-maps-react](http://fullstackreact.github.io/google-maps-react) 92 | 93 | ## Additional Map Props 94 | The Map component takes a number of optional props. 95 | 96 | Zoom: (Shown Above) takes a number with the higher value representing a tighter focus on the map's center. 97 | 98 | Style: Takes CSS style object - commonly width and height. 99 | 100 | ```javascript 101 | const style = { 102 | width: '100%', 103 | height: '100%' 104 | } 105 | ``` 106 | 107 | Container Style: Takes CSS style object - optional, commonly when you want to change from the default of position "absolute". 108 | 109 | ```javascript 110 | const containerStyle = { 111 | position: 'relative', 112 | width: '100%', 113 | height: '100%' 114 | } 115 | ``` 116 | 117 | ```javascript 118 | 135 | ``` 136 | center: Takes an object containing latitude and longitude coordinates. Use this if you want to re-render the map after the initial render. 137 | 138 | ```javascript 139 | 149 | ``` 150 | bounds: Takes a [google.maps.LatLngBounds()](https://developers.google.com/maps/documentation/javascript/reference/3/#LatLngBounds) object to adjust the center and zoom of the map. 151 | ```javascript 152 | var points = [ 153 | { lat: 42.02, lng: -77.01 }, 154 | { lat: 42.03, lng: -77.02 }, 155 | { lat: 41.03, lng: -77.04 }, 156 | { lat: 42.05, lng: -77.02 } 157 | ] 158 | var bounds = new this.props.google.maps.LatLngBounds(); 159 | for (var i = 0; i < points.length; i++) { 160 | bounds.extend(points[i]); 161 | } 162 | return ( 163 | 170 | 171 | ); 172 | 173 | ``` 174 | 175 | The following props are boolean values for map behavior: 176 | `scrollwheel`, `draggable`, `keyboardShortcuts`, `disableDoubleClickZoom` 177 | 178 | The following props are boolean values for presence of controls on the map: 179 | `zoomControl`, `mapTypeControl`, `scaleControl`, `streetViewControl`, `panControl`, `rotateControl`, `fullscreenControl` 180 | 181 | The following props are object values for control options such as placement of controls on the map: 182 | `zoomControlOptions`, `mapTypeControlOptions`, `streetViewControlOptions` 183 | See Google Maps [Controls](https://developers.google.com/maps/documentation/javascript/controls) for more information. 184 | 185 | 186 | It also takes event handlers described below: 187 | 188 | ### Events 189 | 190 | The `` component handles events out of the box. All event handlers are optional. 191 | 192 | #### onReady 193 | 194 | When the `` instance has been loaded and is ready on the page, it will call the `onReady` prop, if given. The `onReady` prop is useful for fetching places or using the autocomplete API for places. 195 | 196 | ```javascript 197 | fetchPlaces(mapProps, map) { 198 | const {google} = mapProps; 199 | const service = new google.maps.places.PlacesService(map); 200 | // ... 201 | } 202 | 203 | render() { 204 | return ( 205 | 208 | 209 | 210 | ) 211 | } 212 | ``` 213 | 214 | #### onClick 215 | 216 | To listen for clicks on the `` component, pass the `onClick` prop: 217 | 218 | ```javascript 219 | mapClicked(mapProps, map, clickEvent) { 220 | // ... 221 | } 222 | 223 | render() { 224 | return ( 225 | 227 | ) 228 | } 229 | ``` 230 | 231 | #### onDragend 232 | 233 | When our user changes the map center by dragging the Map around, we can get a callback after the event is fired with the `onDragend` prop: 234 | 235 | ```javascript 236 | centerMoved(mapProps, map) { 237 | // ... 238 | } 239 | 240 | render() { 241 | return ( 242 | 244 | ) 245 | } 246 | ``` 247 | 248 | The `` component also listens to `onRecenter`, `onBounds_changed`, `onCenter_changed`, `onDblclick`, `onDragstart`, `onHeading_change`, `onIdle`, `onMaptypeid_changed`, `onMousemove`, `onMouseout`, `onMouseover`, `onProjection_changed`, `onResize`, `onRightclick`, `onTilesloaded`, `onTilt_changed`, and `onZoom_changed` events. See Google Maps [Events](https://developers.google.com/maps/documentation/javascript/events) for more information. 249 | 250 | ### Visibility 251 | 252 | You can control the visibility of the map by using the `visible` prop. This is useful for situations when you want to use the Google Maps API without a map. The `` component will load like normal. See the [Google places demo](https://fullstackreact.github.io/google-maps-react/#/places) 253 | 254 | For example: 255 | 256 | ```javascript 257 | 259 | 260 | 261 | ``` 262 | 263 | ## Subcomponents 264 | 265 | The `` api includes subcomponents intended on being used as children of the `Map` component. Any child can be used within the `Map` component and will receive the three `props` (as children): 266 | 267 | * `map` - the Google instance of the `map` 268 | * `google` - a reference to the `window.google` object 269 | * `mapCenter` - the `google.maps.LatLng()` object referring to the center of the map instance 270 | 271 | ### Marker 272 | 273 | To place a marker on the Map, include it as a child of the `` component. 274 | 275 | ```javascript 276 | 280 | 284 | 287 | 288 | 296 | 297 | ``` 298 | 299 | The `` component accepts a `position` prop that defines the location for the `position` on the map. It can be either a raw object or a `google.maps.LatLng()` instance. 300 | 301 | If no `position` is passed in the `props`, the marker will default to the current position of the map, i.e. the `mapCenter` prop. 302 | 303 | You can also pass any other `props` you want with the ``. It will be passed back through marker events. 304 | 305 | The marker component can also accept a child InfoMarker component for situations where there is only 1 marker and 1 infowindow. 306 | 307 | ```javascript 308 | moveMarker(props, marker, e) { 309 | console.log(e.latLng.lat(), e.latLng.lng()) // get the new coordinates after drag end 310 | } 311 | ``` 312 | 313 | ```javascript 314 | 321 | 325 |
326 |

Click on the map or drag the marker to select location where the incident occurred

327 |
328 |
329 |
330 | ``` 331 | 332 | ### Events 333 | 334 | The `` component listens for events, similar to the `` component. 335 | 336 | #### onClick 337 | 338 | You can listen for an `onClick` event with the (appropriately named) `onClick` prop. 339 | 340 | ```javascript 341 | onMarkerClick(props, marker, e) { 342 | // .. 343 | } 344 | 345 | render() { 346 | return ( 347 | 348 | 350 | 351 | ) 352 | } 353 | ``` 354 | 355 | #### mouseover 356 | 357 | You can also pass a callback when the user mouses over a `` instance by passing the `onMouseover` callback: 358 | 359 | ```javascript 360 | onMouseoverMarker(props, marker, e) { 361 | // .. 362 | } 363 | 364 | render() { 365 | return ( 366 | 367 | 369 | 370 | ) 371 | } 372 | ``` 373 | 374 | ### Polygon 375 | 376 | To place a polygon on the Map, set `` as child of Map component. 377 | 378 | ```javascript 379 | render() { 380 | const triangleCoords = [ 381 | {lat: 25.774, lng: -80.190}, 382 | {lat: 18.466, lng: -66.118}, 383 | {lat: 32.321, lng: -64.757}, 384 | {lat: 25.774, lng: -80.190} 385 | ]; 386 | 387 | return( 388 | 392 | 399 | 400 | ) 401 | } 402 | ``` 403 | 404 | #### Events 405 | 406 | The `` component listens to `onClick`, `onMouseover` and `onMouseout` events. 407 | 408 | ### Polyline 409 | 410 | To place a polyline on the Map, set `` as child of Map component. 411 | 412 | ```javascript 413 | render() { 414 | const triangleCoords = [ 415 | {lat: 25.774, lng: -80.190}, 416 | {lat: 18.466, lng: -66.118}, 417 | {lat: 32.321, lng: -64.757}, 418 | {lat: 25.774, lng: -80.190} 419 | ]; 420 | 421 | return( 422 | 426 | 431 | 432 | ) 433 | } 434 | ``` 435 | 436 | #### Events 437 | 438 | The `` component listens to `onClick`, `onMouseover` and `onMouseout` events. 439 | 440 | ### InfoWindow 441 | 442 | The `` component included in this library is gives us the ability to pop up a "more info" window on our Google map. 443 | 444 | ![](http://d.pr/i/16w0V.png) 445 | 446 | The visibility of the `` component is controlled by a `visible` prop. The `visible` prop is a boolean (`PropTypes.bool`) that shows the `` when true and hides it when false. 447 | 448 | There are two ways how to control a position of the `` component. 449 | You can use a `position` prop or connect the `` component directly to an existing `` component by using a `marker` prop. 450 | 451 | ```javascript 452 | //note: code formatted for ES6 here 453 | export class MapContainer extends Component { 454 | state = { 455 | showingInfoWindow: false, 456 | activeMarker: {}, 457 | selectedPlace: {}, 458 | }; 459 | 460 | onMarkerClick = (props, marker, e) => 461 | this.setState({ 462 | selectedPlace: props, 463 | activeMarker: marker, 464 | showingInfoWindow: true 465 | }); 466 | 467 | onMapClicked = (props) => { 468 | if (this.state.showingInfoWindow) { 469 | this.setState({ 470 | showingInfoWindow: false, 471 | activeMarker: null 472 | }) 473 | } 474 | }; 475 | 476 | render() { 477 | return ( 478 | 480 | 482 | 483 | 486 |
487 |

{this.state.selectedPlace.name}

488 |
489 |
490 |
491 | ) 492 | } 493 | } 494 | ``` 495 | 496 | ### Events 497 | 498 | The `` throws events when it's showing/hiding. Every event is optional and can accept a handler to be called when the event is fired. 499 | 500 | ```javascript 501 | 505 |
506 |

{this.state.selectedPlace.name}

507 |
508 |
509 | ``` 510 | 511 | #### onClose 512 | 513 | The `onClose` event is fired when the `` has been closed. It's useful for changing state in the parent component to keep track of the state of the ``. 514 | 515 | #### onOpen 516 | 517 | The `onOpen` event is fired when the window has been mounted in the Google map instance. It's useful for keeping track of the state of the `` from within the parent component. 518 | 519 | ### Circle 520 | 521 | ![A red slightly transparent circle on a Google Map. The map is centered around an area in Sao Paulo, Brazil and there is a peculiar lake on the map that is shaped like a man.](examples/screenshots/circle.png "Circle") 522 | 523 | To place a circle on the Map, set `` as child of Map component. 524 | 525 | ```javascript 526 | render() { 527 | const coords = { lat: -21.805149, lng: -49.0921657 }; 528 | 529 | return ( 530 | 536 | console.log('mouseover')} 540 | onClick={() => console.log('click')} 541 | onMouseout={() => console.log('mouseout')} 542 | strokeColor='transparent' 543 | strokeOpacity={0} 544 | strokeWeight={5} 545 | fillColor='#FF0000' 546 | fillOpacity={0.2} 547 | /> 548 | 549 | ); 550 | } 551 | ``` 552 | 553 | #### Events 554 | 555 | The `` component listens to `onClick`, `onMouseover` and `onMouseout` events. 556 | 557 | The `GoogleApiWrapper` automatically passes the `google` instance loaded when the component mounts (and will only load it once). 558 | 559 | #### Custom Map Style 560 | 561 | To set your own custom map style, import your custom map style in JSON format. 562 | 563 | ```javascript 564 | const mapStyle = [ 565 | { 566 | featureType: 'landscape.man_made', 567 | elementType: 'geometry.fill', 568 | stylers: [ 569 | { 570 | color: '#dceafa' 571 | } 572 | ] 573 | }, 574 | ] 575 | 576 | _mapLoaded(mapProps, map) { 577 | map.setOptions({ 578 | styles: mapStyle 579 | }) 580 | } 581 | 582 | render() { 583 | return ( 584 | this._mapLoaded(mapProps, map)} 590 | > 591 | ... 592 | 593 | ); 594 | } 595 | 596 | ``` 597 | 598 | ## Manually loading the Google API 599 | 600 | If you prefer not to use the automatic loading option, you can also pass the `window.google` instance as a `prop` to your `` component. 601 | 602 | ```javascript 603 | 604 | ``` 605 | 606 | ## Issues? 607 | 608 | If you have some issues, please make an issue on the issues tab and try to include an example. We've had success with https://codesandbox.io 609 | 610 | An example template might look like: [https://codesandbox.io/s/rzwrk2854](https://codesandbox.io/s/rzwrk2854) 611 | 612 | ## Contributing 613 | 614 | ```shell 615 | git clone https://github.com/fullstackreact/google-maps-react.git 616 | cd google-maps-react 617 | npm install 618 | make dev 619 | ``` 620 | 621 | The Google Map React component library uses React and the Google API to give easy access to the Google Maps library. 622 | 623 | ___ 624 | 625 | # Fullstack React Book 626 | 627 | 628 | Fullstack React Book 629 | 630 | 631 | This Google Map React component library was built alongside the blog post [How to Write a Google Maps React Component](https://www.fullstackreact.com/articles/how-to-write-a-google-maps-react-component/). 632 | 633 | This repo was written and is maintained by the [Fullstack React](https://fullstackreact.com) team. In the book we cover many more projects like this. We walk through each line of code, explain why it's there and how it works. 634 | 635 | This app is only one of several apps we have in the book. If you're looking to learn React, there's no faster way than by spending a few hours with the Fullstack React book. 636 | 637 |
638 | 639 | ## License 640 | [MIT](/LICENSE) 641 | --------------------------------------------------------------------------------