new: google map react

This commit is contained in:
cybice 2015-06-10 15:16:41 +03:00
commit 8fdc6dd692
23 changed files with 1533 additions and 0 deletions

3
.babelrc Normal file
View File

@ -0,0 +1,3 @@
{
"stage": 0
}

232
.eslintrc Normal file
View File

@ -0,0 +1,232 @@
{
"parser": 'babel-eslint',
"env": {
"browser": true,
"node": true,
"es6": true
},
"globals": {
"__DEV__": false
},
"ecmaFeatures": {
"arrowFunctions": true,
"binaryLiterals": true,
"blockBindings": true,
"classes": true,
"defaultParams": true,
"destructuring": true,
"forOf": true,
"generators": true,
"modules": true,
"objectLiteralComputedProperties": true,
"objectLiteralDuplicateProperties": true,
"objectLiteralShorthandMethods": true,
"objectLiteralShorthandProperties": true,
"octalLiterals": true,
"regexUFlag": true,
"regexYFlag": true,
"spread": true,
"superInFunctions": true,
"templateStrings": true,
"unicodeCodePointEscapes": true,
"globalReturn": true,
"jsx": true
},
"plugins": [
"react"
],
"rules": {
"react/display-name": 0,
"react/jsx-boolean-value": 1,
"react/jsx-quotes": 1,
"react/jsx-no-undef": 1,
"react/jsx-sort-props": 0,
"react/jsx-sort-prop-types": 0,
"react/jsx-uses-react": 1,
"react/jsx-uses-vars": 1,
"react/no-did-mount-set-state": 1,
"react/no-did-update-set-state": 1,
"react/no-multi-comp": 1,
"react/no-unknown-property": 1,
"react/prop-types": 1,
"react/react-in-jsx-scope": 1,
"react/self-closing-comp": 1,
"react/wrap-multilines": 1,
/**
* Strict mode
*/
// babel inserts "use strict"; for us
// http://eslint.org/docs/rules/strict
"strict": [2, "never"],
/**
* ES6
*/
"no-var": 2, // http://eslint.org/docs/rules/no-var
/**
* Variables
*/
"no-shadow": 1, // http://eslint.org/docs/rules/no-shadow
"no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names
"no-unused-vars": [
1,
{
// http://eslint.org/docs/rules/no-unused-vars
"vars": "local",
"args": "after-used"
}
],
"no-use-before-define": 2, // http://eslint.org/docs/rules/no-use-before-define
/**
* Possible errors
*/
"comma-dangle": [2, "never"], // http://eslint.org/docs/rules/comma-dangle
"no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign
"no-console": 1, // http://eslint.org/docs/rules/no-console
"no-debugger": 1, // http://eslint.org/docs/rules/no-debugger
"no-alert": 1, // http://eslint.org/docs/rules/no-alert
"no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition
"no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys
"no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case
"no-empty": 2, // http://eslint.org/docs/rules/no-empty
"no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign
"no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast
"no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi
"no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign
"no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations
"no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp
"no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace
"no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls
"no-reserved-keys": 2, // http://eslint.org/docs/rules/no-reserved-keys
"no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays
"no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable
"use-isnan": 2, // http://eslint.org/docs/rules/use-isnan
"block-scoped-var": 2, // http://eslint.org/docs/rules/block-scoped-var
/**
* Best practices
*/
"consistent-return": 2, // http://eslint.org/docs/rules/consistent-return
"curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly
"default-case": 2, // http://eslint.org/docs/rules/default-case
"dot-notation": [
2,
{
// http://eslint.org/docs/rules/dot-notation
"allowKeywords": true
}
],
"eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq
"guard-for-in": 2, // http://eslint.org/docs/rules/guard-for-in
"no-caller": 2, // http://eslint.org/docs/rules/no-caller
"no-else-return": 2, // http://eslint.org/docs/rules/no-else-return
"no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null
"no-eval": 2, // http://eslint.org/docs/rules/no-eval
"no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native
"no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind
"no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough
"no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal
"no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval
"no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks
"no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func
"no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str
"no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign
"no-new": 2, // http://eslint.org/docs/rules/no-new
"no-new-func": 2, // http://eslint.org/docs/rules/no-new-func
"no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers
"no-octal": 2, // http://eslint.org/docs/rules/no-octal
"no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape
"no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign
"no-proto": 2, // http://eslint.org/docs/rules/no-proto
"no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare
"no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign
"no-script-url": 2, // http://eslint.org/docs/rules/no-script-url
"no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare
"no-sequences": 2, // http://eslint.org/docs/rules/no-sequences
"no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal
"no-with": 2, // http://eslint.org/docs/rules/no-with
"radix": 2, // http://eslint.org/docs/rules/radix
"vars-on-top": 2, // http://eslint.org/docs/rules/vars-on-top
"wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife
"yoda": 2, // http://eslint.org/docs/rules/yoda
/**
* Style
*/
"indent": [2, 2], // http://eslint.org/docs/rules/
"brace-style": [
2, // http://eslint.org/docs/rules/brace-style
"1tbs",
{
"allowSingleLine": true
}
],
"quotes": [
2, "single", "avoid-escape" // http://eslint.org/docs/rules/quotes
],
"camelcase": [
2,
{
// http://eslint.org/docs/rules/camelcase
"properties": "never"
}
],
"comma-spacing": [
2,
{
// http://eslint.org/docs/rules/comma-spacing
"before": false,
"after": true
}
],
"comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style
"eol-last": 2, // http://eslint.org/docs/rules/eol-last
"func-names": 1, // http://eslint.org/docs/rules/func-names
"key-spacing": [
2,
{
// http://eslint.org/docs/rules/key-spacing
"beforeColon": false,
"afterColon": true
}
],
"new-cap": [
2,
{
// http://eslint.org/docs/rules/new-cap
"newIsCap": true
}
],
"no-multiple-empty-lines": [
2,
{
// http://eslint.org/docs/rules/no-multiple-empty-lines
"max": 2
}
],
"no-nested-ternary": 2, // http://eslint.org/docs/rules/no-nested-ternary
"no-new-object": 2, // http://eslint.org/docs/rules/no-new-object
"no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func
"no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces
"no-wrap-func": 2, // http://eslint.org/docs/rules/no-wrap-func
"no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle
"one-var": [2, "never"], // http://eslint.org/docs/rules/one-var
"padded-blocks": [2, "never"], // http://eslint.org/docs/rules/padded-blocks
"semi": [2, "always"], // http://eslint.org/docs/rules/semi
"semi-spacing": [
2,
{
// http://eslint.org/docs/rules/semi-spacing
"before": false,
"after": true
}
],
"space-after-keywords": 2, // http://eslint.org/docs/rules/space-after-keywords
"space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks
"space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren
"space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops
"space-return-throw-case": 2, // http://eslint.org/docs/rules/space-return-throw-case
"spaced-line-comment": 2 // http://eslint.org/docs/rules/spaced-line-comment
}
}

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/node_modules
/modules

6
.npmignore Normal file
View File

@ -0,0 +1,6 @@
src
scripts
__tests__
examples
.babelrc
.eslintrc

2
README.md Normal file
View File

@ -0,0 +1,2 @@
#google map react
isomorphic google map react component, allows render react components on the google map

47
package.json Normal file
View File

@ -0,0 +1,47 @@
{
"name": "google-map-react",
"version": "0.3.0",
"description": "isomorphic google map react component, allows render react components on the google map",
"main": "modules/google_map.js",
"scripts": {
"build": "./scripts/build.sh",
"prepublish": "npm run build",
"eyetest": "babel-node ./src/__tests__/eye_test.js",
"es5eyetest": "node ./modules/__tests__/eye_test.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/istarkov/google-map-react.git"
},
"keywords": [
"react",
"reactjs",
"google",
"map",
"maps",
"isomorphic",
"render",
"component",
"javascript",
"react-component"
],
"author": "istarkov https://github.com/istarkov",
"license": "MIT",
"bugs": {
"url": "https://github.com/istarkov/google-map-react/issues"
},
"homepage": "https://github.com/istarkov/google-map-react#readme",
"dependencies": {
"eventemitter3": "^1.1.0",
"point-geometry": "0.0.0",
"react-pure-render": "^1.0.1",
"scriptjs": "^2.5.7"
},
"peerDependencies": {
"react": ">=0.13.0 <0.15.0"
},
"devDependencies": {
"babel": "^5.5.6",
"react": "^0.13.3"
}
}

3
scripts/build.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
rm -rf modules
./node_modules/.bin/babel src --out-dir modules

37
src/__tests__/eye_test.js Normal file
View File

@ -0,0 +1,37 @@
// "eye test" (c)somebody - means check output by your eyes :-)
// TODO read how to test react components
import React, {PropTypes, Component} from 'react';
import GoogleMap from '../google_map.js';
export default class SimpleTest extends Component {
static propTypes = {
center: PropTypes.array,
zoom: PropTypes.number,
greatPlaceCoords: PropTypes.any
};
static defaultProps = {
center: [59.938043, 30.337157],
zoom: 9,
greatPlaceCoords: {lat: 59.724465, lng: 30.080121}
};
constructor(props) {
super(props);
}
render() {
return (
<GoogleMap
center={this.props.center}
zoom={this.props.zoom}>
<div lat={59.955413} lng={30.337844}>----------I-PROMISE-TO---------------</div>
<div {...this.props.greatPlaceCoords}>-------WRITE-SOME-TESTS-------------</div>
</GoogleMap>
);
}
}
const html = React.renderToString(<SimpleTest />);
console.log(html); // eslint-disable-line no-console

436
src/google_map.js Normal file
View File

@ -0,0 +1,436 @@
import React, {PropTypes, Component} from 'react';
import shouldPureComponentUpdate from 'react-pure-render/function';
import MarkerDispatcher from './marker_dispatcher.js';
import GoogleMapMap from './google_map_map.js';
import GoogleMapMarkers from './google_map_markers.js';
import GoogleMapMarkersPrerender from './google_map_markers_prerender.js';
import googleMapLoader from './utils/loaders/google_map_loader.js';
import detectBrowser from './utils/detect.js';
import Geo from './utils/geo.js';
import isArraysEqualEps from './utils/array_helper.js';
const kEPS = 0.00001;
const K_GOOGLE_TILE_SIZE = 256;
const K_MAP_CONTROL_OPTIONS = {
overviewMapControl: false,
streetViewControl: false,
rotateControl: true,
mapTypeControl: false,
// disable poi
styles: [{ featureType: 'poi', elementType: 'labels', stylers: [{ visibility: 'off' }]}],
minZoom: 3 // i need to dynamically calculate possible zoom value
};
const style = {
width: '100%',
height: '100%',
margin: 0,
padding: 0,
position: 'relative'
};
function isNumber(n) {
return !Number.isNaN(parseFloat(n)) && Number.isFinite(n);
}
export default class GoogleMap extends Component {
static propTypes = {
apiKey: PropTypes.string,
center: PropTypes.array.isRequired,
zoom: PropTypes.number.isRequired,
onBoundsChange: PropTypes.func,
onChildClick: PropTypes.func,
onChildMouseEnter: PropTypes.func,
onChildMouseLeave: PropTypes.func,
options: PropTypes.any,
distanceToMouse: PropTypes.func,
hoverDistance: PropTypes.number,
debounced: PropTypes.bool,
margin: PropTypes.array,
googleMapLoader: PropTypes.any
};
static defaultProps = {
distanceToMouse(pt, mousePos /*, markerProps*/) {
const x = pt.x;
const y = pt.y; // - 20;
return Math.sqrt((x - mousePos.x) * (x - mousePos.x) + (y - mousePos.y) * (y - mousePos.y));
},
hoverDistance: 30,
debounced: true,
options: K_MAP_CONTROL_OPTIONS,
googleMapLoader
};
shouldComponentUpdate = shouldPureComponentUpdate;
constructor(props) {
super(props);
this.mounted_ = false;
this.map_ = null;
this.maps_ = null;
this.prevBounds_ = null;
this.mouse_ = null;
this.mouseMoveTime_ = 0;
this.boundingRect_ = null;
this.mouseInMap_ = true;
this.dragTime_ = 0;
this.fireMouseEventOnIdle_ = false;
this.updateCounter_ = 0;
this.markersDispatcher_ = new MarkerDispatcher(this);
this.geoService_ = new Geo(K_GOOGLE_TILE_SIZE);
if (this._isCenterDefined(this.props.center)) {
this.geoService_.setView(this.props.center, this.props.zoom, 0);
}
this.state = {
overlayCreated: false
};
}
_initMap = () => {
const center = this.props.center;
this.geoService_.setView(center, this.props.zoom, 0);
this._onBoundsChanged(); // now we can calculate map bounds center etc...
this.props.googleMapLoader(this.props.apiKey)
.then(maps => {
if (!this.mounted_) {
return;
}
const centerLatLng = this.geoService_.getCenter();
const propsOptions = {
zoom: this.props.zoom,
center: new maps.LatLng(centerLatLng.lat, centerLatLng.lng)
};
const mapOptions = {...this.props.options, ...propsOptions};
const map = new maps.Map(React.findDOMNode(this.refs.google_map_dom), mapOptions);
this.map_ = map;
this.maps_ = maps;
// render in overlay
const this_ = this;
const overlay = Object.assign(new maps.OverlayView(), {
onAdd() {
const K_MAX_WIDTH = (typeof screen !== 'undefined') ? `${screen.width}px` : '2000px';
const K_MAX_HEIGHT = (typeof screen !== 'undefined') ? `${screen.height}px` : '2000px';
const div = document.createElement('div');
this.div = div;
div.style.backgroundColor = 'transparent';
div.style.position = 'absolute';
div.style.left = '0px';
div.style.top = '0px';
div.style.width = K_MAX_WIDTH; // prevents some chrome draw defects
div.style.height = K_MAX_HEIGHT;
const panes = this.getPanes();
panes.overlayMouseTarget.appendChild(div);
React.render((
<GoogleMapMarkers
onChildClick={this_._onChildClick}
onChildMouseEnter={this_._onChildMouseEnter}
onChildMouseLeave={this_._onChildMouseLeave}
geoService={this_.geoService_}
projectFromLeftTop={true}
distanceToMouse={this_.props.distanceToMouse}
hoverDistance={this_.props.hoverDistance}
dispatcher={this_.markersDispatcher_} />),
div,
() => {
// remove prerendered markers
this_.setState({overlayCreated: true});
}
);
},
draw() {
const div = overlay.div;
const overlayProjection = overlay.getProjection();
const bounds = map.getBounds();
const ne = bounds.getNorthEast();
const sw = bounds.getSouthWest();
const ptx = overlayProjection.fromLatLngToDivPixel(new maps.LatLng(ne.lat(), sw.lng()));
// need round for safari still can't find what need for firefox
const ptxRounded = detectBrowser().isSafari ? {x: Math.round(ptx.x), y: Math.round(ptx.y)} : {x: ptx.x, y: ptx.y};
this_.updateCounter_++;
this_._onBoundsChanged(map, maps, !this_.props.debounced);
div.style.left = `${ptxRounded.x}px`;
div.style.top = `${ptxRounded.y}px`;
if (this_.markersDispatcher_) {
this_.markersDispatcher_.emit('kON_CHANGE');
}
}
});
overlay.setMap(map);
maps.event.addListener(map, 'idle', () => {
if (this.resetSizeOnIdle_) {
this._setViewSize();
this.resetSizeOnIdle_ = false;
}
const div = overlay.div;
const overlayProjection = overlay.getProjection();
const bounds = map.getBounds();
const ne = bounds.getNorthEast();
const sw = bounds.getSouthWest();
const ptx = overlayProjection.fromLatLngToDivPixel(new maps.LatLng(ne.lat(), sw.lng()));
// need round for safari still can't find what need for firefox
const ptxRounded = detectBrowser().isSafari ? {x: Math.round(ptx.x), y: Math.round(ptx.y)} : {x: ptx.x, y: ptx.y};
this_.updateCounter_++;
this_._onBoundsChanged(map, maps);
this_.dragTime_ = 0;
div.style.left = `${ptxRounded.x}px`;
div.style.top = `${ptxRounded.y}px`;
if (this_.markersDispatcher_) {
this_.markersDispatcher_.emit('kON_CHANGE');
if (this_.fireMouseEventOnIdle_) {
this_.markersDispatcher_.emit('kON_MOUSE_POSITION_CHANGE');
}
}
});
maps.event.addListener(map, 'mouseover', () => { // has advantage over div MouseLeave
this_.mouseInMap_ = true;
});
maps.event.addListener(map, 'mouseout', () => { // has advantage over div MouseLeave
this_.mouseInMap_ = false;
this_.mouse_ = null;
this_.markersDispatcher_.emit('kON_MOUSE_POSITION_CHANGE');
});
maps.event.addListener(map, 'drag', () => {
this_.dragTime_ = (new Date()).getTime();
});
})
.catch( e => {
console.error(e); // eslint-disable-line no-console
throw e;
});
}
_onChildClick = (...args) => {
if (this.props.onChildClick) {
return this.props.onChildClick(...args);
}
}
_onChildMouseEnter = (...args) => {
if (this.props.onChildMouseEnter) {
return this.props.onChildMouseEnter(...args);
}
}
_onChildMouseLeave = (...args) => {
if (this.props.onChildMouseLeave) {
return this.props.onChildMouseLeave(...args);
}
}
_setViewSize = () => {
const mapDom = React.findDOMNode(this.refs.google_map_dom);
this.geoService_.setViewSize(mapDom.clientWidth, mapDom.clientHeight);
this._onBoundsChanged();
}
_onWindowResize = () => {
this.resetSizeOnIdle_ = true;
}
_onBoundsChanged = (map, maps, callExtBoundsChange) => {
if (map) {
const gmC = map.getCenter();
this.geoService_.setView([gmC.lat(), gmC.lng()], map.getZoom(), 0);
}
if (this.props.onBoundsChange && this.geoService_.canProject()) {
const zoom = this.geoService_.getZoom();
const bounds = this.geoService_.getBounds();
const centerLatLng = this.geoService_.getCenter();
if (!isArraysEqualEps(bounds, this.prevBounds_, kEPS)) {
if (callExtBoundsChange !== false) {
const marginBounds = this.geoService_.getBounds(this.props.margin);
this.props.onBoundsChange([centerLatLng.lat, centerLatLng.lng], zoom, bounds, marginBounds);
this.prevBounds_ = bounds;
}
}
// uncomment for strange bugs
if (process.env.NODE_ENV !== 'production') { // compare with google calculations
if (map) {
const locBounds = map.getBounds();
const ne = locBounds.getNorthEast();
const sw = locBounds.getSouthWest();
const gmC = map.getCenter();
// compare with google map
if (!isArraysEqualEps([centerLatLng.lat, centerLatLng.lng], [gmC.lat(), gmC.lng()], kEPS)) {
console.info('GoogleMap center not eq:', [centerLatLng.lat, centerLatLng.lng], [gmC.lat(), gmC.lng()]); // eslint-disable-line no-console
}
if (!isArraysEqualEps(bounds, [ne.lat(), sw.lng(), sw.lat(), ne.lng()], kEPS)) {
// this is normal if this message occured on resize
console.info('GoogleMap bounds not eq:', '\n', bounds, '\n', [ne.lat(), sw.lng(), sw.lat(), ne.lng()]); // eslint-disable-line no-console
}
}
}
}
}
_onMouseMove = (e) => {
if (!this.mouseInMap_) return;
const currTime = (new Date()).getTime();
const K_RECALC_CLIENT_RECT_MS = 3000;
if (currTime - this.mouseMoveTime_ > K_RECALC_CLIENT_RECT_MS) {
this.boundingRect_ = e.currentTarget.getBoundingClientRect();
}
this.mouseMoveTime_ = currTime;
const mousePosX = e.clientX - this.boundingRect_.left;
const mousePosY = e.clientY - this.boundingRect_.top;
if (!this.mouse_) {
this.mouse_ = {x: 0, y: 0, lat: 0, lng: 0};
}
const K_IDLE_TIMEOUT = 100;
this.mouse_.x = mousePosX;
this.mouse_.y = mousePosY;
const latLng = this.geoService_.unproject(this.mouse_, true);
this.mouse_.lat = latLng.lat;
this.mouse_.lng = latLng.lng;
if (currTime - this.dragTime_ < K_IDLE_TIMEOUT) {
this.fireMouseEventOnIdle_ = true;
} else {
this.markersDispatcher_.emit('kON_MOUSE_POSITION_CHANGE');
this.fireMouseEventOnIdle_ = false;
}
}
_onMapClick = () => {
if (this.markersDispatcher_) {
const K_IDLE_TIMEOUT = 100;
const currTime = (new Date()).getTime();
if (currTime - this.dragTime_ > K_IDLE_TIMEOUT) {
this.markersDispatcher_.emit('kON_CLICK');
}
}
}
_isCenterDefined = (center) => {
return center && center.length === 2 && isNumber(center[0]) && isNumber(center[1]);
}
componentDidMount() {
this.mounted_ = true;
window.addEventListener('resize', this._onWindowResize);
setTimeout(() => { // to detect size
this._setViewSize();
if (this._isCenterDefined(this.props.center)) {
this._initMap();
} else {
this.props.googleMapLoader(this.props.apiKey); // начать подгружать можно уже сейчас
}
}, 0, this);
}
componentWillUnmount() {
this.mounted_ = false;
window.removeEventListener('resize', this._onWindowResize);
if (this.maps_ && this.map_) {
this.maps_.event.clearInstanceListeners(this.map_);
}
this.map_ = null;
this.maps_ = null;
this.markersDispatcher_.dispose();
this.resetSizeOnIdle_ = false;
delete this.map_;
delete this.markersDispatcher_;
}
componentWillReceiveProps(nextProps) {
if (!this._isCenterDefined(this.props.center) && this._isCenterDefined(nextProps.center)) {
setTimeout(() =>
this._initMap(), 0);
}
if (this.map_) {
const centerLatLng = this.geoService_.getCenter();
if (nextProps.center) {
if (Math.abs(nextProps.center[0] - centerLatLng.lat) + Math.abs(nextProps.center[1] - centerLatLng.lng) > kEPS) {
this.map_.panTo({lat: nextProps.center[0], lng: nextProps.center[1]});
}
}
// if zoom chaged by user
if (Math.abs(nextProps.zoom - this.props.zoom) > 0) {
this.map_.setZoom(nextProps.zoom);
}
}
}
componentDidUpdate() {
this.markersDispatcher_.emit('kON_CHANGE');
}
render() {
const mapMarkerPrerender = !this.state.overlayCreated ? (
<GoogleMapMarkersPrerender
onChildClick={this._onChildClick}
onChildMouseEnter={this._onChildMouseEnter}
onChildMouseLeave={this._onChildMouseLeave}
geoService={this.geoService_}
projectFromLeftTop={false}
distanceToMouse={this.props.distanceToMouse}
hoverDistance={this.props.hoverDistance}
dispatcher={this.markersDispatcher_} />
) : null;
return (
<div style={style} onMouseMove={this._onMouseMove} onClick={this._onMapClick}>
<GoogleMapMap ref="google_map_dom" />
{/*render markers before map load done*/}
{mapMarkerPrerender}
</div>
);
}
}

27
src/google_map_map.js Normal file
View File

@ -0,0 +1,27 @@
import React, {Component} from 'react';
const style = {
width: '100%',
height: '100%',
left: 0,
top: 0,
margin: 0,
padding: 0,
position: 'absolute'
};
export default class GoogleMapMap extends Component {
constructor(props) {
super(props);
}
shouldComponentUpdate() {
return false; // disable react on this div
}
render() {
return (
<div style={style} />
);
}
}

232
src/google_map_markers.js Normal file
View File

@ -0,0 +1,232 @@
import React, {PropTypes, Component} from 'react';
import shouldPureComponentUpdate from 'react-pure-render/function';
const mainStyle = {
width: '100%',
height: '100%',
left: 0,
top: 0,
margin: 0,
padding: 0,
position: 'absolute'
};
const style = {
width: 0,
height: 0,
left: 0,
top: 0,
backgroundColor: 'transparent',
position: 'absolute'
};
export default class GoogleMapMarkers extends Component {
static propTypes = {
geoService: PropTypes.any,
style: PropTypes.any,
distanceToMouse: PropTypes.func,
dispatcher: PropTypes.any,
onChildClick: PropTypes.func,
onChildMouseLeave: PropTypes.func,
onChildMouseEnter: PropTypes.func,
hoverDistance: PropTypes.number,
projectFromLeftTop: PropTypes.bool
};
static defaultProps = {
projectFromLeftTop: false
};
shouldComponentUpdate = shouldPureComponentUpdate;
constructor(props) {
super(props);
this.props.dispatcher.on('kON_CHANGE', this._onChangeHandler);
this.props.dispatcher.on('kON_MOUSE_POSITION_CHANGE', this._onMouseChangeHandler);
this.props.dispatcher.on('kON_CLICK', this._onChildClick);
this.dimesionsCache_ = {};
this.hoverKey_ = null;
this.hoverChildProps_ = null;
this.allowMouse_ = true;
this.state = {...this._getState(), hoverKey: null};
}
_getState = () => {
return {
children: this.props.dispatcher.getChildren(),
updateCounter: this.props.dispatcher.getUpdateCounter()
};
}
_onChangeHandler = () => {
if (!this.dimesionsCache_) {
return;
}
const state = this._getState();
this.setState(state);
}
_onChildClick = () => {
if (this.props.onChildClick) {
if (this.hoverChildProps_) {
const hoverKey = this.hoverKey_;
const childProps = this.hoverChildProps_;
// click works only on hovered item
this.props.onChildClick(hoverKey, childProps);
}
}
}
_onChildMouseEnter = (hoverKey, childProps) => {
if (!this.dimesionsCache_) {
return;
}
if (this.props.onChildMouseEnter) {
this.props.onChildMouseEnter(hoverKey, childProps);
}
this.hoverChildProps_ = childProps;
this.hoverKey_ = hoverKey;
this.setState({hoverKey: hoverKey});
}
_onChildMouseLeave = () => {
if (!this.dimesionsCache_) {
return;
}
const hoverKey = this.hoverKey_;
const childProps = this.hoverChildProps_;
if (hoverKey !== undefined && hoverKey !== null) {
if (this.props.onChildMouseLeave) {
this.props.onChildMouseLeave(hoverKey, childProps);
}
this.hoverKey_ = null;
this.hoverChildProps_ = null;
this.setState({hoverKey: null});
}
}
_onMouseAllow = (value) => {
if (!value) {
this._onChildMouseLeave();
}
this.allowMouse_ = value;
}
_onMouseChangeHandler = () => {
if (this.allowMouse_) {
this._onMouseChangeHandler_raf();
}
}
_onMouseChangeHandler_raf = () => {
if (!this.dimesionsCache_) {
return;
}
const mp = this.props.dispatcher.getMousePosition();
if (mp) {
let distances = [];
React.Children.forEach(this.state.children, (child, childIndex) => {
const childKey = child.key !== undefined && child.key !== null ? child.key : childIndex;
const dist = this.props.distanceToMouse(this.dimesionsCache_[childKey], mp, child.props);
if (dist < this.props.hoverDistance) {
distances.push(
{
key: childKey,
dist: dist,
props: child.props
});
}
});
if (distances.length) {
distances.sort((a, b) => a.dist - b.dist);
const hoverKey = distances[0].key;
const childProps = distances[0].props;
if (this.hoverKey_ !== hoverKey) {
this._onChildMouseLeave();
this._onChildMouseEnter(hoverKey, childProps);
}
} else {
this._onChildMouseLeave();
}
} else {
this._onChildMouseLeave();
}
}
_getDimensions = (key) => {
const childKey = key;
return this.dimesionsCache_[childKey];
}
componentWillUnmount() {
this.props.dispatcher.removeListener('kON_CHANGE', this._onChangeHandler);
this.props.dispatcher.removeListener('kON_MOUSE_POSITION_CHANGE', this._onMouseChangeHandler);
this.props.dispatcher.removeListener('kON_CLICK', this._onChildClick);
this.dimesionsCache_ = null;
}
render() {
const mainElementStyle = this.props.style || mainStyle;
this.dimesionsCache_ = {};
const markers = React.Children.map(this.state.children, (child, childIndex) => {
const pt = this.props.geoService.project({lat: child.props.lat, lng: child.props.lng}, this.props.projectFromLeftTop);
const stylePtPos = {
left: pt.x,
top: pt.y
};
let dx = 0;
let dy = 0;
if (!this.props.projectFromLeftTop) { // center projection
if (this.props.geoService.hasSize()) {
dx = this.props.geoService.getWidth() / 2;
dy = this.props.geoService.getHeight() / 2;
}
}
// to prevent rerender on child element i need to pass const params $getDimensions and $dimensionKey instead of dimension object
const childKey = child.key !== undefined && child.key !== null ? child.key : childIndex;
this.dimesionsCache_[childKey] = {x: pt.x + dx, y: pt.y + dy, lat: child.props.lat, lng: child.props.lng};
return (
<div key={childKey} style={{...style, ...stylePtPos}}>
{React.cloneElement(child, {
$hover: childKey === this.state.hoverKey,
$getDimensions: this._getDimensions,
$dimensionKey: childKey,
$geoService: this.props.geoService,
$onMouseAllow: this._onMouseAllow
})}
</div>
);
});
return (
<div style={mainElementStyle}>
{markers}
</div>
);
}
}

View File

@ -0,0 +1,29 @@
import React, {PropTypes, Component} from 'react';
import GoogleMapMarkers from './google_map_markers.js';
const style = {
width: '50%',
height: '50%',
left: '50%',
top: '50%',
// backgroundColor: 'red',
margin: 0,
padding: 0,
position: 'absolute'
// opacity: 0.3
};
export default class GoogleMapMarkersPrerender extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div style={style}>
<GoogleMapMarkers {...this.props} />
</div>
);
}
}

25
src/marker_dispatcher.js Normal file
View File

@ -0,0 +1,25 @@
import EventEmitter from 'eventemitter3';
export default class MarkerDispatcher extends EventEmitter {
constructor(gmapInstance) {
super();
this.gmapInstance = gmapInstance;
}
getChildren() {
return this.gmapInstance.props.children;
}
getMousePosition() {
return this.gmapInstance.mouse_;
}
getUpdateCounter() {
return this.gmapInstance.updateCounter_;
}
dispose() {
this.gmapInstance = null;
this.removeAllListeners();
}
}

12
src/utils/array_helper.js Normal file
View File

@ -0,0 +1,12 @@
export default function isArraysEqualEps(arrayA, arrayB, eps) {
if (arrayA && arrayB) {
for (let i = 0; i !== arrayA.length; ++i) {
if (Math.abs(arrayA[i] - arrayB[i]) > eps) {
return false;
}
}
return true;
}
return false;
}

31
src/utils/detect.js Normal file
View File

@ -0,0 +1,31 @@
// code here http://stackoverflow.com/questions/5899783/detect-safari-chrome-ie-firefox-opera-with-user-agent
let detectBrowserResult_ = null;
export default function detectBrowser() {
if (detectBrowserResult_) {
return detectBrowserResult_;
}
if (typeof navigator !== 'undefined') {
const isExplorer = navigator.userAgent.indexOf('MSIE') > -1;
const isFirefox = navigator.userAgent.indexOf('Firefox') > -1;
const isOpera = navigator.userAgent.toLowerCase().indexOf('op') > -1;
let isChrome = navigator.userAgent.indexOf('Chrome') > -1;
let isSafari = navigator.userAgent.indexOf('Safari') > -1;
if ((isChrome) && (isSafari)) {
isSafari = false;
}
if ((isChrome) && (isOpera)) {
isChrome = false;
}
detectBrowserResult_ = {isExplorer, isFirefox, isOpera, isChrome, isSafari};
return detectBrowserResult_;
}
detectBrowserResult_ = {isChrome: true, isExplorer: false, isFirefox: false, isOpera: false, isSafari: false};
return detectBrowserResult_;
}

107
src/utils/geo.js Normal file
View File

@ -0,0 +1,107 @@
import LatLng from './lib_geo/lat_lng.js';
import Point from 'point-geometry';
import Transform from './lib_geo/transform.js';
export default class Geo {
constructor(tileSize) { // left_top view пользует гугл
// super();
this.hasSize_ = false;
this.hasView_ = false;
this.transform_ = new Transform(tileSize || 512);
}
setView(center, zoom, bearing) {
this.transform_.center = LatLng.convert(center);
this.transform_.zoom = +zoom;
this.transform_.bearing = +bearing;
this.hasView_ = true;
}
setViewSize(width, height) {
this.transform_.width = width;
this.transform_.height = height;
this.hasSize_ = true;
}
canProject() {
return this.hasSize_ && this.hasView_;
}
hasSize() {
return this.hasSize_;
}
unproject(ptXY, viewFromLeftTop) {
let ptRes;
if (viewFromLeftTop) {
const ptxy = Object.assign({}, ptXY);
ptxy.x -= this.transform_.width / 2;
ptxy.y -= this.transform_.height / 2;
ptRes = this.transform_.pointLocation(Point.convert(ptxy));
} else {
ptRes = this.transform_.pointLocation(Point.convert(ptXY));
}
ptRes.lng -= 360 * Math.round(ptRes.lng / 360); // convert 2 google format
return ptRes;
}
project(ptLatLng, viewFromLeftTop) {
if (viewFromLeftTop) {
const pt = this.transform_.locationPoint(LatLng.convert(ptLatLng));
pt.x -= this.transform_.worldSize * Math.round(pt.x / this.transform_.worldSize);
pt.x += this.transform_.width / 2;
pt.y += this.transform_.height / 2;
return pt;
}
return this.transform_.locationPoint(LatLng.convert(ptLatLng));
}
getWidth() {
return this.transform_.width;
}
getHeight() {
return this.transform_.height;
}
getZoom() {
return this.transform_.zoom;
}
getCenter() {
let ptRes = this.transform_.pointLocation({x: 0, y: 0});
return ptRes;
}
getBounds(margins, roundFactor) {
const bndT = margins && margins[0] || 0;
const bndR = margins && margins[1] || 0;
const bndB = margins && margins[2] || 0;
const bndL = margins && margins[3] || 0;
if (this.getWidth() - bndR - bndL > 0 && this.getHeight() - bndT - bndB > 0) {
const topLeftCorner = this.unproject({x: bndL - this.getWidth() / 2, y: bndT - this.getHeight() / 2});
const bottomRightCorner = this.unproject({x: this.getWidth() / 2 - bndR, y: this.getHeight() / 2 - bndB});
let res = [
topLeftCorner.lat, topLeftCorner.lng,
bottomRightCorner.lat, bottomRightCorner.lng
];
if (roundFactor) {
res = res.map(r => Math.round(r * roundFactor) / roundFactor);
}
return res;
}
return [0, 0, 0, 0];
}
}

View File

@ -0,0 +1,27 @@
Copyright (c) 2014, Mapbox
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of Mapbox GL JS nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,29 @@
'use strict';
module.exports = LatLng;
var wrap = require('./wrap.js').wrap;
function LatLng(lat, lng) {
if (isNaN(lat) || isNaN(lng)) {
throw new Error('Invalid LatLng object: (' + lat + ', ' + lng + ')');
}
this.lat = +lat;
this.lng = +lng;
}
LatLng.prototype.wrap = function () {
return new LatLng(this.lat, wrap(this.lng, -180, 180));
};
// constructs LatLng from an array if necessary
LatLng.convert = function (a) {
if (a instanceof LatLng) {
return a;
}
if (Array.isArray(a)) {
return new LatLng(a[0], a[1]);
}
return a;
};

View File

@ -0,0 +1,72 @@
'use strict';
module.exports = LatLngBounds;
var LatLng = require('./lat_lng');
function LatLngBounds(sw, ne) {
if (!sw) return;
var latlngs = ne ? [sw, ne] : sw;
for (var i = 0, len = latlngs.length; i < len; i++) {
this.extend(latlngs[i]);
}
}
LatLngBounds.prototype = {
// extend the bounds to contain the given point or bounds
extend: function (obj) {
var sw = this._sw,
ne = this._ne,
sw2, ne2;
if (obj instanceof LatLng) {
sw2 = obj;
ne2 = obj;
} else if (obj instanceof LatLngBounds) {
sw2 = obj._sw;
ne2 = obj._ne;
if (!sw2 || !ne2) return this;
} else {
return obj ? this.extend(LatLng.convert(obj) || LatLngBounds.convert(obj)) : this;
}
if (!sw && !ne) {
this._sw = new LatLng(sw2.lat, sw2.lng);
this._ne = new LatLng(ne2.lat, ne2.lng);
} else {
sw.lat = Math.min(sw2.lat, sw.lat);
sw.lng = Math.min(sw2.lng, sw.lng);
ne.lat = Math.max(ne2.lat, ne.lat);
ne.lng = Math.max(ne2.lng, ne.lng);
}
return this;
},
getCenter: function () {
return new LatLng((this._sw.lat + this._ne.lat) / 2, (this._sw.lng + this._ne.lng) / 2);
},
getSouthWest: function () { return this._sw; },
getNorthEast: function () { return this._ne; },
getNorthWest: function () { return new LatLng(this.getNorth(), this.getWest()); },
getSouthEast: function () { return new LatLng(this.getSouth(), this.getEast()); },
getWest: function () { return this._sw.lng; },
getSouth: function () { return this._sw.lat; },
getEast: function () { return this._ne.lng; },
getNorth: function () { return this._ne.lat; }
};
// constructs LatLngBounds from an array if necessary
LatLngBounds.convert = function (a) {
if (!a || a instanceof LatLngBounds) return a;
return new LatLngBounds(a);
};

View File

@ -0,0 +1,2 @@
###Код выдран из mapboxgl чтобы преобразования координат не зависели от библиотеки

View File

@ -0,0 +1,121 @@
const LatLng = require('./lat_lng');
const Point = require('point-geometry');
const wrap = require('./wrap.js').wrap;
// A single transform, generally used for a single tile to be scaled, rotated, and zoomed.
function Transform(tileSize, minZoom, maxZoom) {
this.tileSize = tileSize || 512; // constant
this._minZoom = minZoom || 0;
this._maxZoom = maxZoom || 52;
this.latRange = [-85.05113, 85.05113];
this.width = 0;
this.height = 0;
this.zoom = 0;
this.center = new LatLng(0, 0);
this.angle = 0;
}
Transform.prototype = {
get minZoom() { return this._minZoom; },
set minZoom(zoom) {
this._minZoom = zoom;
this.zoom = Math.max(this.zoom, zoom);
},
get maxZoom() { return this._maxZoom; },
set maxZoom(zoom) {
this._maxZoom = zoom;
this.zoom = Math.min(this.zoom, zoom);
},
get worldSize() {
return this.tileSize * this.scale;
},
get centerPoint() {
return new Point(0, 0); // this.size._div(2);
},
get size() {
return new Point(this.width, this.height);
},
get bearing() {
return -this.angle / Math.PI * 180;
},
set bearing(bearing) {
this.angle = -wrap(bearing, -180, 180) * Math.PI / 180;
},
get zoom() { return this._zoom; },
set zoom(zoom) {
zoom = Math.min(Math.max(zoom, this.minZoom), this.maxZoom);
this._zoom = zoom;
this.scale = this.zoomScale(zoom);
this.tileZoom = Math.floor(zoom);
this.zoomFraction = zoom - this.tileZoom;
},
zoomScale: function(zoom) { return Math.pow(2, zoom); },
scaleZoom: function(scale) { return Math.log(scale) / Math.LN2; },
project: function(latlng, worldSize) {
return new Point(
this.lngX(latlng.lng, worldSize),
this.latY(latlng.lat, worldSize));
},
unproject: function(point, worldSize) {
return new LatLng(
this.yLat(point.y, worldSize),
this.xLng(point.x, worldSize));
},
get x() {
return this.lngX(this.center.lng);
},
get y() {
return this.latY(this.center.lat);
},
get point() { return new Point(this.x, this.y); },
// lat/lon <-> absolute pixel coords convertion
lngX: function(lon, worldSize) {
return (180 + lon) * (worldSize || this.worldSize) / 360;
},
// latitude to absolute y coord
latY: function(lat, worldSize) {
var y = 180 / Math.PI * Math.log(Math.tan(Math.PI / 4 + lat * Math.PI / 360));
return (180 - y) * (worldSize || this.worldSize) / 360;
},
xLng: function(x, worldSize) {
return x * 360 / (worldSize || this.worldSize) - 180;
},
yLat: function(y, worldSize) {
var y2 = 180 - y * 360 / (worldSize || this.worldSize);
return 360 / Math.PI * Math.atan(Math.exp(y2 * Math.PI / 180)) - 90;
},
locationPoint: function(latlng) {
var p = this.project(latlng);
return this.centerPoint._sub(this.point._sub(p)._rotate(this.angle));
},
pointLocation: function(p) {
var p2 = this.centerPoint._sub(p)._rotate(-this.angle);
return this.unproject(this.point.sub(p2));
},
};
module.exports = Transform;

View File

@ -0,0 +1,6 @@
'use strict';
exports.wrap = function (n, min, max) {
var d = max - min;
return n === max ? n : ((n - min) % d + d) % d + min;
};

View File

@ -0,0 +1,45 @@
let $script_ = null;
let _loadPromise;
// TODO add libraries language and other map options
module.exports = function googleMapLoader(apiKey) {
if (!$script_) {
$script_ = require('scriptjs');
}
if (_loadPromise) {
return _loadPromise;
}
_loadPromise = new Promise((resolve, reject) => {
if (typeof window === 'undefined') {
reject(new Error('google map cannot be loaded outside browser env'));
return;
}
if (window.google && window.google.maps) {
resolve(window.google.maps);
return;
}
if (typeof window._$_google_map_initialize_$_ !== 'undefined') {
reject(new Error('google map initialization error'));
}
window._$_google_map_initialize_$_ = () => {
delete window._$_google_map_initialize_$_;
resolve(window.google.maps);
};
const apiKeyString = apiKey ? `&key=${apiKey}` : '';
$script_(`https://maps.googleapis.com/maps/api/js?callback=_$_google_map_initialize_$_${apiKeyString}`, () => {
if (typeof window.google === 'undefined') {
reject(new Error('google map initialization error (not loaded)'));
}
});
});
return _loadPromise;
};