import React, { PropTypes, Component } from 'react'; import ReactDOM from 'react-dom'; import shallowEqual from 'fbjs/lib/shallowEqual'; import MarkerDispatcher from './marker_dispatcher'; import GoogleMapMap from './google_map_map'; import GoogleMapMarkers from './google_map_markers'; import GoogleMapMarkersPrerender from './google_map_markers_prerender'; import googleMapLoader from './utils/loaders/google_map_loader'; import detectBrowser from './utils/detect'; import Geo from './utils/geo'; import isArraysEqualEps from './utils/array_helper'; import isPlainObject from './utils/is_plain_object'; import pick from './utils/pick'; import raf from './utils/raf'; import log2 from './utils/math/log2'; import isNumber from './utils/isNumber'; import omit from './utils/omit'; import detectElementResize from './utils/detectElementResize'; const kEPS = 0.00001; const K_GOOGLE_TILE_SIZE = 256; // real minZoom calculated here _getMinZoom const K_IDLE_TIMEOUT = 100; const K_IDLE_CLICK_TIMEOUT = 300; const DEFAULT_MIN_ZOOM = 3; function defaultOptions_(/* maps */) { return { overviewMapControl: false, streetViewControl: false, rotateControl: true, mapTypeControl: false, // disable poi styles: [{ featureType: 'poi', elementType: 'labels', stylers: [{ visibility: 'off' }] }], minZoom: DEFAULT_MIN_ZOOM, // dynamically recalculted if possible during init }; } const latLng2Obj = (latLng) => isPlainObject(latLng) ? latLng : { lat: latLng[0], lng: latLng[1] }; export default class GoogleMap extends Component { static propTypes = { apiKey: PropTypes.string, bootstrapURLKeys: PropTypes.any, defaultCenter: React.PropTypes.oneOfType([ PropTypes.array, PropTypes.shape({ lat: PropTypes.number, lng: PropTypes.number, }), ]), center: React.PropTypes.oneOfType([ PropTypes.array, PropTypes.shape({ lat: PropTypes.number, lng: PropTypes.number, }), ]), defaultZoom: PropTypes.number, zoom: PropTypes.number, onBoundsChange: PropTypes.func, onChange: PropTypes.func, onClick: PropTypes.func, onChildClick: PropTypes.func, onChildMouseDown: PropTypes.func, onChildMouseUp: PropTypes.func, onChildMouseMove: PropTypes.func, onChildMouseEnter: PropTypes.func, onChildMouseLeave: PropTypes.func, onZoomAnimationStart: PropTypes.func, onZoomAnimationEnd: PropTypes.func, onDrag: PropTypes.func, options: PropTypes.any, distanceToMouse: PropTypes.func, hoverDistance: PropTypes.number, debounced: PropTypes.bool, margin: PropTypes.array, googleMapLoader: PropTypes.any, onGoogleApiLoaded: PropTypes.func, yesIWantToUseGoogleMapApiInternals: PropTypes.bool, draggable: PropTypes.bool, style: PropTypes.any, resetBoundsOnResize: PropTypes.bool, layerTypes: PropTypes.arrayOf(PropTypes.string), // ['TransitLayer', 'TrafficLayer'] }; static defaultProps = { distanceToMouse(pt, mousePos /* , markerProps */) { return Math.sqrt( (pt.x - mousePos.x) * (pt.x - mousePos.x) + (pt.y - mousePos.y) * (pt.y - mousePos.y) ); }, hoverDistance: 30, debounced: true, options: defaultOptions_, googleMapLoader, yesIWantToUseGoogleMapApiInternals: false, style: { width: '100%', height: '100%', margin: 0, padding: 0, position: 'relative', }, layerTypes: [], }; static googleMapLoader = googleMapLoader; // eslint-disable-line constructor(props) { super(props); this.mounted_ = false; this.initialized_ = false; this.googleApiLoadedCalled_ = 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); this.centerIsObject_ = isPlainObject(this.props.center); this.minZoom_ = DEFAULT_MIN_ZOOM; this.defaultDraggableOption_ = true; this.zoomControlClickTime_ = 0; this.childMouseDownArgs_ = null; this.childMouseUpTime_ = 0; if (process.env.NODE_ENV !== 'production') { if (this.props.apiKey) { console.warn('GoogleMap: ' + // eslint-disable-line no-console 'apiKey is deprecated, use ' + 'bootstrapURLKeys={{key: YOUR_API_KEY}} instead.'); } if (this.props.onBoundsChange) { console.warn('GoogleMap: ' + // eslint-disable-line no-console 'onBoundsChange is deprecated, use ' + 'onChange({center, zoom, bounds, ...other}) instead.'); } if (this.props.center === undefined && this.props.defaultCenter === undefined) { console.warn('GoogleMap: center or defaultCenter' + // eslint-disable-line no-console 'property must be defined'); } if (this.props.zoom === undefined && this.props.defaultZoom === undefined) { console.warn('GoogleMap: zoom or defaultZoom' + // eslint-disable-line no-console 'property must be defined'); } } if (this._isCenterDefined(this.props.center || this.props.defaultCenter)) { const propsCenter = latLng2Obj(this.props.center || this.props.defaultCenter); this.geoService_.setView(propsCenter, this.props.zoom || this.props.defaultZoom, 0); } this.zoomAnimationInProgress_ = false; this.state = { overlayCreated: false, }; } componentDidMount() { this.mounted_ = true; window.addEventListener('resize', this._onWindowResize); window.addEventListener('keydown', this._onKeyDownCapture, true); // prevent touch devices from moving the entire browser window on drag window.addEventListener('touchmove', this._onTouchMove); const mapDom = ReactDOM.findDOMNode(this.refs.google_map_dom); // gmap can't prevent map drag if mousedown event already occured // the only workaround I find is prevent mousedown native browser event ReactDOM.findDOMNode(this.refs.google_map_dom) .addEventListener('mousedown', this._onMapMouseDownNative, true); window.addEventListener('mouseup', this._onChildMouseUp, false); const bootstrapURLKeys = { ...(this.props.apiKey && { key: this.props.apiKey }), ...this.props.bootstrapURLKeys, }; this.props.googleMapLoader(bootstrapURLKeys); // we can start load immediatly setTimeout(() => { // to detect size this._setViewSize(); if (this._isCenterDefined(this.props.center || this.props.defaultCenter)) { this._initMap(); } }, 0, this); if (this.props.resetBoundsOnResize) { const that = this; detectElementResize.addResizeListener(mapDom, that._mapDomResizeCallback); } } componentWillReceiveProps(nextProps) { if (process.env.NODE_ENV !== 'production') { if (this.props.defaultCenter !== nextProps.defaultCenter) { console.warn('GoogleMap: defaultCenter prop changed. ' + // eslint-disable-line 'You can\'t change default props.'); } if (this.props.defaultZoom !== nextProps.defaultZoom) { console.warn('GoogleMap: defaultZoom prop changed. ' + // eslint-disable-line 'You can\'t change default props.'); } } if (!this._isCenterDefined(this.props.center) && this._isCenterDefined(nextProps.center)) { setTimeout(() => this._initMap(), 0); } if (this.map_) { const centerLatLng = this.geoService_.getCenter(); if (this._isCenterDefined(nextProps.center)) { const nextPropsCenter = latLng2Obj(nextProps.center); const currCenter = this._isCenterDefined(this.props.center) ? latLng2Obj(this.props.center) : null; if ( !currCenter || Math.abs(nextPropsCenter.lat - currCenter.lat) + Math.abs(nextPropsCenter.lng - currCenter.lng) > kEPS ) { if ( Math.abs(nextPropsCenter.lat - centerLatLng.lat) + Math.abs(nextPropsCenter.lng - centerLatLng.lng) > kEPS ) { this.map_.panTo({ lat: nextPropsCenter.lat, lng: nextPropsCenter.lng }); } } } if (nextProps.zoom !== undefined) { // if zoom chaged by user if (Math.abs(nextProps.zoom - this.props.zoom) > 0) { this.map_.setZoom(nextProps.zoom); } } if (this.props.draggable !== undefined && nextProps.draggable === undefined) { // reset to default this.map_.setOptions({ draggable: this.defaultDraggableOption_ }); } else if (this.props.draggable !== nextProps.draggable) { // also prevent this on window 'mousedown' event to prevent map move this.map_.setOptions({ draggable: nextProps.draggable }); } // use shallowEqual to try avoid calling map._setOptions if only the ref changes if (nextProps.options !== undefined && !shallowEqual(this.props.options, nextProps.options)) { const mapPlainObjects = pick(this.maps_, isPlainObject); let options = typeof nextProps.options === 'function' ? nextProps.options(mapPlainObjects) : nextProps.options; // remove zoom, center and draggable options as these are managed by google-maps-react options = omit(options, ['zoom', 'center', 'draggable']); if (options.hasOwnProperty('minZoom')) { const minZoom = this._computeMinZoom(options.minZoomOverride, options.minZoom); options.minZoom = this._checkMinZoom(options.minZoom, minZoom); } this.map_.setOptions(options); } } } shouldComponentUpdate(nextProps, nextState) { // draggable does not affect inner components return !shallowEqual( omit(this.props, ['draggable']), omit(nextProps, ['draggable']) ) || !shallowEqual(this.state, nextState); } componentDidUpdate(prevProps) { this.markersDispatcher_.emit('kON_CHANGE'); if (this.props.hoverDistance !== prevProps.hoverDistance) { this.markersDispatcher_.emit('kON_MOUSE_POSITION_CHANGE'); } } componentWillUnmount() { this.mounted_ = false; const that = this; const mapDom = ReactDOM.findDOMNode(this.refs.google_map_dom); window.removeEventListener('resize', this._onWindowResize); window.removeEventListener('keydown', this._onKeyDownCapture); mapDom.removeEventListener('mousedown', this._onMapMouseDownNative, true); window.removeEventListener('mouseup', this._onChildMouseUp, false); window.removeEventListener('touchmove', this._onTouchMove); detectElementResize.addResizeListener(mapDom, that._mapDomResizeCallback); if (this.overlay_) { // this triggers overlay_.onRemove(), which will unmount the this.overlay_.setMap(null); } 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_; } // calc minZoom if map size available // it's better to not set minZoom less than this calculation gives // otherwise there is no homeomorphism between screen coordinates and map // (one map coordinate can have different screen coordinates) _getMinZoom = () => { if (this.geoService_.getWidth() > 0 || this.geoService_.getHeight() > 0) { const tilesPerWidth = Math.ceil(this.geoService_.getWidth() / K_GOOGLE_TILE_SIZE) + 2; const tilesPerHeight = Math.ceil(this.geoService_.getHeight() / K_GOOGLE_TILE_SIZE) + 2; const maxTilesPerDim = Math.max(tilesPerWidth, tilesPerHeight); return Math.ceil(log2(maxTilesPerDim)); } return DEFAULT_MIN_ZOOM; } _computeMinZoom = (minZoomOverride, minZoom) => { if (minZoomOverride) { return minZoom || DEFAULT_MIN_ZOOM; } return this._getMinZoom(); } _checkMinZoom(zoom, minZoom) { if (process.env.NODE_ENV !== 'production') { if (zoom < minZoom) { console.warn( 'GoogleMap: ' + // eslint-disable-line 'minZoom option is less than recommended ' + 'minZoom option for your map sizes.\n' + 'overrided to value ' + minZoom); } } if (minZoom < zoom) { return zoom; } return minZoom; } _mapDomResizeCallback = () => { this.resetSizeOnIdle_ = true; if (this.maps_) { this.maps_.event.trigger(this.map_, 'resize'); } } _initMap = () => { // only initialize the map once if (this.initialized_) { return; } this.initialized_ = true; const propsCenter = latLng2Obj(this.props.center || this.props.defaultCenter); this.geoService_.setView(propsCenter, this.props.zoom || this.props.defaultZoom, 0); this._onBoundsChanged(); // now we can calculate map bounds center etc... const bootstrapURLKeys = { ...(this.props.apiKey && { key: this.props.apiKey }), ...this.props.bootstrapURLKeys, }; this.props.googleMapLoader(bootstrapURLKeys) .then(maps => { if (!this.mounted_) { return; } const centerLatLng = this.geoService_.getCenter(); const propsOptions = { zoom: this.props.zoom || this.props.defaultZoom, center: new maps.LatLng(centerLatLng.lat, centerLatLng.lng), }; // prevent to exapose full api // next props must be exposed (console.log(Object.keys(pick(maps, isPlainObject)))) // "Animation", "ControlPosition", "MapTypeControlStyle", "MapTypeId", // "NavigationControlStyle", "ScaleControlStyle", "StrokePosition", // "SymbolPath", "ZoomControlStyle", // "event", "DirectionsStatus", "DirectionsTravelMode", "DirectionsUnitSystem", // "DistanceMatrixStatus", // "DistanceMatrixElementStatus", "ElevationStatus", "GeocoderLocationType", // "GeocoderStatus", "KmlLayerStatus", // "MaxZoomStatus", "StreetViewStatus", "TransitMode", "TransitRoutePreference", // "TravelMode", "UnitSystem" const mapPlainObjects = pick(maps, isPlainObject); const options = typeof this.props.options === 'function' ? this.props.options(mapPlainObjects) : this.props.options; const defaultOptions = defaultOptions_(mapPlainObjects); const draggableOptions = this.props.draggable !== undefined && { draggable: this.props.draggable }; const minZoom = this._computeMinZoom(options.minZoomOverride, options.minZoom); this.minZoom_ = minZoom; const preMapOptions = { ...defaultOptions, minZoom, ...options, ...propsOptions, }; this.defaultDraggableOption_ = preMapOptions.draggable !== undefined ? preMapOptions.draggable : this.defaultDraggableOption_; const mapOptions = { ...preMapOptions, ...draggableOptions, }; mapOptions.minZoom = this._checkMinZoom(mapOptions.minZoom, minZoom); const map = new maps.Map(ReactDOM.findDOMNode(this.refs.google_map_dom), mapOptions); this.props.layerTypes.forEach((layerType) => { const layer = new maps[layerType](); layer.setMap(map); }); this.map_ = map; this.maps_ = maps; // render in overlay const this_ = this; const overlay = this.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); ReactDOM.unstable_renderSubtreeIntoContainer( this_, , div, // remove prerendered markers () => this_.setState({ overlayCreated: true }), ); }, onRemove() { ReactDOM.unmountComponentAtNode(this.div); }, 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); if (!this_.googleApiLoadedCalled_) { this_._onGoogleApiLoaded({ map, maps }); this_.googleApiLoadedCalled_ = true; } 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, 'zoom_changed', () => { // recalc position at zoom start if (this_.geoService_.getZoom() !== map.getZoom()) { if (!this_.zoomAnimationInProgress_) { this_.zoomAnimationInProgress_ = true; this_._onZoomAnimationStart(); } const TIMEOUT_ZOOM = 300; if ((new Date()).getTime() - this.zoomControlClickTime_ < TIMEOUT_ZOOM) { // there is strange Google Map Api behavior in chrome when zoom animation of map // is started only on second raf call, if was click on zoom control // or +- keys pressed, so i wait for two rafs before change state // this does not fully prevent animation jump // but reduce it's occurence probability raf(() => raf(() => { this_.updateCounter_++; this_._onBoundsChanged(map, maps); })); } else { this_.updateCounter_++; this_._onBoundsChanged(map, maps); } } }); maps.event.addListener(map, 'idle', () => { if (this.resetSizeOnIdle_) { this._setViewSize(); const currMinZoom = this._computeMinZoom( this.props.options.minZoomOverride, this.props.options.minZoom); if (currMinZoom !== this.minZoom_) { this.minZoom_ = currMinZoom; map.setOptions({ minZoom: currMinZoom }); } this.resetSizeOnIdle_ = false; } if (this_.zoomAnimationInProgress_) { this_.zoomAnimationInProgress_ = false; this_._onZoomAnimationEnd(); } 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); if (this.mouse_) { const latLng = this.geoService_.unproject(this.mouse_, true); this.mouse_.lat = latLng.lat; this.mouse_.lng = latLng.lng; } this._onChildMouseMove(); 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(); this_._onDrag(); }); }) .catch(e => { console.error(e); // eslint-disable-line no-console throw e; }); } _onGoogleApiLoaded = (...args) => { if (this.props.onGoogleApiLoaded) { if (process.env.NODE_ENV !== 'production' && this.props.yesIWantToUseGoogleMapApiInternals !== true) { console.warn( 'GoogleMap: ' + // eslint-disable-line 'Usage of internal api objects is dangerous ' + 'and can cause a lot of issues.\n' + 'To hide this warning add yesIWantToUseGoogleMapApiInternals={true} ' + 'to this.props.hoverDistance; _onDrag = (...args) => this.props.onDrag && this.props.onDrag(...args); _onZoomAnimationStart = (...args) => this.props.onZoomAnimationStart && this.props.onZoomAnimationStart(...args) _onZoomAnimationEnd = (...args) => this.props.onZoomAnimationEnd && this.props.onZoomAnimationEnd(...args) _onChildClick = (...args) => { if (this.props.onChildClick) { return this.props.onChildClick(...args); } return undefined; } _onChildMouseDown = (hoverKey, childProps) => { this.childMouseDownArgs_ = [hoverKey, childProps]; if (this.props.onChildMouseDown) { this.props.onChildMouseDown(hoverKey, childProps, { ...this.mouse_ }); } } // this method works only if this.props.onChildMouseDown was called _onChildMouseUp = () => { if (this.childMouseDownArgs_) { if (this.props.onChildMouseUp) { this.props.onChildMouseUp(...this.childMouseDownArgs_, { ...this.mouse_ }); } this.childMouseDownArgs_ = null; this.childMouseUpTime_ = (new Date()).getTime(); } } // this method works only if this.props.onChildMouseDown was called _onChildMouseMove = () => { if (this.childMouseDownArgs_) { if (this.props.onChildMouseMove) { this.props.onChildMouseMove(...this.childMouseDownArgs_, { ...this.mouse_ }); } } } _onChildMouseEnter = (...args) => { if (this.props.onChildMouseEnter) { return this.props.onChildMouseEnter(...args); } return undefined; } _onChildMouseLeave = (...args) => { if (this.props.onChildMouseLeave) { return this.props.onChildMouseLeave(...args); } return undefined; } _setViewSize = () => { if (!this.mounted_) return; const mapDom = ReactDOM.findDOMNode(this.refs.google_map_dom); this.geoService_.setViewSize(mapDom.clientWidth, mapDom.clientHeight); this._onBoundsChanged(); } _onWindowResize = () => { this.resetSizeOnIdle_ = true; } _onMapMouseMove = (e) => { if (!this.mouseInMap_) return; const currTime = (new Date()).getTime(); const K_RECALC_CLIENT_RECT_MS = 50; 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 }; } 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; this._onChildMouseMove(); if (currTime - this.dragTime_ < K_IDLE_TIMEOUT) { this.fireMouseEventOnIdle_ = true; } else { this.markersDispatcher_.emit('kON_MOUSE_POSITION_CHANGE'); this.fireMouseEventOnIdle_ = false; } } // K_IDLE_CLICK_TIMEOUT - looks like 300 is enough _onClick = (...args) => this.props.onClick && !this.childMouseDownArgs_ && ((new Date()).getTime() - this.childMouseUpTime_) > K_IDLE_CLICK_TIMEOUT && this.dragTime_ === 0 && this.props.onClick(...args) _onMapClick = (event) => { if (this.markersDispatcher_) { // support touch events and recalculate mouse position on click this._onMapMouseMove(event); const currTime = (new Date()).getTime(); if (currTime - this.dragTime_ > K_IDLE_TIMEOUT) { if (this.mouse_) { this._onClick({ ...this.mouse_, event, }); } this.markersDispatcher_.emit('kON_CLICK', event); } } } // gmap can't prevent map drag if mousedown event already occured // the only workaround I find is prevent mousedown native browser event _onMapMouseDownNative = (event) => { if (!this.mouseInMap_) return; this._onMapMouseDown(event); } _onMapMouseDown = (event) => { if (this.markersDispatcher_) { const currTime = (new Date()).getTime(); if (currTime - this.dragTime_ > K_IDLE_TIMEOUT) { // Hovered marker detected at mouse move could be deleted at mouse down time // so it will be good to force hovered marker recalculation this._onMapMouseMove(event); this.markersDispatcher_.emit('kON_MDOWN', event); } } } _onMapMouseDownCapture = () => { if (detectBrowser().isChrome) { // to fix strange zoom in chrome if (!this.mouse_) { this.zoomControlClickTime_ = (new Date()).getTime(); } } } _onKeyDownCapture = () => { if (detectBrowser().isChrome) { this.zoomControlClickTime_ = (new Date()).getTime(); } } _onTouchMove = (event) => { if (this.refs.google_map_dom) { const mapDom = ReactDOM.findDOMNode(this.refs.google_map_dom); if (mapDom.contains(event.target)) { event.preventDefault(); } } } _isCenterDefined = (center) => center && ( (isPlainObject(center) && isNumber(center.lat) && isNumber(center.lng)) || (center.length === 2 && isNumber(center[0]) && isNumber(center[1])) ) _onBoundsChanged = (map, maps, callExtBoundsChange) => { if (map) { const gmC = map.getCenter(); this.geoService_.setView([gmC.lat(), gmC.lng()], map.getZoom(), 0); } if ((this.props.onChange || 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); if (this.props.onBoundsChange) { this.props.onBoundsChange( this.centerIsObject_ ? { ...centerLatLng } : [centerLatLng.lat, centerLatLng.lng], zoom, bounds, marginBounds ); } if (this.props.onChange) { this.props.onChange({ center: { ...centerLatLng }, zoom, bounds: { nw: { lat: bounds[0], lng: bounds[1], }, se: { lat: bounds[2], lng: bounds[3], }, }, marginBounds: { nw: { lat: marginBounds[0], lng: marginBounds[1], }, se: { lat: marginBounds[2], lng: marginBounds[3], }, }, size: this.geoService_.hasSize() ? { width: this.geoService_.getWidth(), height: this.geoService_.getHeight(), } : { width: 0, height: 0, }, }); } this.prevBounds_ = bounds; } } } } render() { const mapMarkerPrerender = !this.state.overlayCreated ? ( ) : null; return (
{/* render markers before map load done */} {mapMarkerPrerender}
); } }