/* eslint-disable import/no-extraneous-dependencies, react/forbid-prop-types, react/no-find-dom-node, no-console, no-undef */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; // helpers import GoogleMapMap from './google_map_map'; import MarkerDispatcher from './marker_dispatcher'; import GoogleMapMarkers from './google_map_markers'; import GoogleMapMarkersPrerender from './google_map_markers_prerender'; import { generateHeatmap, optionsHeatmap } from './google_heatmap'; // loaders import googleMapLoader from './loaders/google_map_loader'; // lib import Geo from './lib/geo'; // tools import raf from './utils/raf'; import log2 from './utils/log2'; import omit from './utils/omit'; import pick from './utils/pick'; import isEmpty from './utils/isEmpty'; import isNumber from './utils/isNumber'; import detectBrowser from './utils/detect'; import shallowEqual from './utils/shallowEqual'; import isPlainObject from './utils/isPlainObject'; import isArraysEqualEps from './utils/isArraysEqualEps'; import { addResizeListener, removeResizeListener, } from './utils/detectElementResize'; import addPassiveEventListener from './utils/passiveEvents'; // consts 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; // Starting with version 3.32, the maps API calls `draw()` each frame during // a zoom animation. const DRAW_CALLED_DURING_ANIMATION_VERSION = 32; const IS_REACT_16 = ReactDOM.createPortal !== undefined; const createPortal = IS_REACT_16 ? ReactDOM.createPortal : ReactDOM.unstable_renderSubtreeIntoContainer; 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] }; const _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; }; const isFullScreen = () => document.fullscreen || document.webkitIsFullScreen || document.mozFullScreen || document.msFullscreenElement; class GoogleMap extends Component { static propTypes = { apiKey: PropTypes.string, bootstrapURLKeys: PropTypes.any, defaultCenter: PropTypes.oneOfType([ PropTypes.array, PropTypes.shape({ lat: PropTypes.number, lng: PropTypes.number, }), ]), center: 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, onDragEnd: PropTypes.func, onMapTypeIdChange: PropTypes.func, onTilesLoaded: 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'] shouldUnregisterMapOnUnmount: PropTypes.bool, }; 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: [], heatmap: {}, heatmapLibrary: false, shouldUnregisterMapOnUnmount: true, }; 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.heatmap = null; this.layers_ = {}; 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; this.googleMapDom_ = null; 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 (isEmpty(this.props.center) && isEmpty(this.props.defaultCenter)) { console.warn( 'GoogleMap: center or defaultCenter property must be defined' // eslint-disable-line no-console ); } if (isEmpty(this.props.zoom) && isEmpty(this.props.defaultZoom)) { console.warn( 'GoogleMap: zoom or defaultZoom property must be defined' // eslint-disable-line no-console ); } } 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 = { overlay: null, }; } componentDidMount() { this.mounted_ = true; addPassiveEventListener(window, 'resize', this._onWindowResize, false); addPassiveEventListener(window, 'keydown', this._onKeyDownCapture, true); const mapDom = ReactDOM.findDOMNode(this.googleMapDom_); // gmap can't prevent map drag if mousedown event already occured // the only workaround I find is prevent mousedown native browser event if (mapDom) { addPassiveEventListener( mapDom, 'mousedown', this._onMapMouseDownNative, true ); } addPassiveEventListener(window, 'mouseup', this._onChildMouseUp, false); const bootstrapURLKeys = { ...(this.props.apiKey && { key: this.props.apiKey }), ...this.props.bootstrapURLKeys, }; this.props.googleMapLoader(bootstrapURLKeys, this.props.heatmapLibrary); // 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; addResizeListener(mapDom, that._mapDomResizeCallback); } } shouldComponentUpdate(nextProps, nextState) { // draggable does not affect inner components return ( !shallowEqual( omit(this.props, ['draggable']), omit(nextProps, ['draggable']) ) || !shallowEqual(this.state, nextState) ); } componentDidUpdate(prevProps) { if (process.env.NODE_ENV !== 'production') { if (!shallowEqual(prevProps.defaultCenter, this.props.defaultCenter)) { console.warn( "GoogleMap: defaultCenter prop changed. You can't change default props." ); } if (!shallowEqual(prevProps.defaultZoom, this.props.defaultZoom)) { console.warn( "GoogleMap: defaultZoom prop changed. You can't change default props." ); } } if ( !this._isCenterDefined(prevProps.center) && this._isCenterDefined(this.props.center) ) { setTimeout(() => this._initMap(), 0); } if (this.map_) { const centerLatLng = this.geoService_.getCenter(); if (this._isCenterDefined(this.props.center)) { const currentCenter = latLng2Obj(this.props.center); const prevCenter = this._isCenterDefined(prevProps.center) ? latLng2Obj(prevProps.center) : null; if ( !prevCenter || Math.abs(currentCenter.lat - prevCenter.lat) + Math.abs(currentCenter.lng - prevCenter.lng) > kEPS ) { if ( Math.abs(currentCenter.lat - centerLatLng.lat) + Math.abs(currentCenter.lng - centerLatLng.lng) > kEPS ) { this.map_.panTo({ lat: currentCenter.lat, lng: currentCenter.lng, }); } } } if (!isEmpty(this.props.zoom)) { // if zoom chaged by user if (Math.abs(this.props.zoom - prevProps.zoom) > 0) { this.map_.setZoom(this.props.zoom); } } if (!isEmpty(prevProps.draggable) && isEmpty(this.props.draggable)) { // reset to default this.map_.setOptions({ draggable: this.defaultDraggableOption_ }); } else if (!shallowEqual(prevProps.draggable, this.props.draggable)) { // also prevent this on window 'mousedown' event to prevent map move this.map_.setOptions({ draggable: this.props.draggable }); } // use shallowEqual to try avoid calling map._setOptions if only the ref changes if ( !isEmpty(this.props.options) && !shallowEqual(prevProps.options, this.props.options) ) { const mapPlainObjects = pick(this.maps_, isPlainObject); let options = typeof this.props.options === 'function' ? this.props.options(mapPlainObjects) : this.props.options; // remove zoom, center and draggable options as these are managed by google-maps-react options = omit(options, ['zoom', 'center', 'draggable']); if ('minZoom' in options) { const minZoom = this._computeMinZoom(options.minZoom); options.minZoom = _checkMinZoom(options.minZoom, minZoom); } this.map_.setOptions(options); } if (!shallowEqual(this.props.layerTypes, prevProps.layerTypes)) { Object.keys(this.layers_).forEach((layerKey) => { this.layers_[layerKey].setMap(null); delete this.layers_[layerKey]; }); this._setLayers(this.props.layerTypes); } if ( this.heatmap && !shallowEqual(this.props.heatmap.positions, prevProps.heatmap.positions) ) { this.heatmap.setData( this.props.heatmap.positions.map((p) => ({ location: new this.maps_.LatLng(p.lat, p.lng), weight: p.weight, })) ); } if ( this.heatmap && !shallowEqual(this.props.heatmap.options, prevProps.heatmap.options) ) { Object.keys(this.props.heatmap.options).forEach((option) => { this.heatmap.set(option, this.props.heatmap.options[option]); }); } } // emit actions this.markersDispatcher_.emit('kON_CHANGE'); if (!shallowEqual(this.props.hoverDistance, prevProps.hoverDistance)) { this.markersDispatcher_.emit('kON_MOUSE_POSITION_CHANGE'); } } componentWillUnmount() { this.mounted_ = false; const mapDom = ReactDOM.findDOMNode(this.googleMapDom_); if (mapDom) { mapDom.removeEventListener('mousedown', this._onMapMouseDownNative, true); } window.removeEventListener('resize', this._onWindowResize); window.removeEventListener('keydown', this._onKeyDownCapture); window.removeEventListener('mouseup', this._onChildMouseUp, false); if (this.props.resetBoundsOnResize) { removeResizeListener(mapDom, this._mapDomResizeCallback); } if (this.overlay_) { // this triggers overlay_.onRemove(), which will unmount the this.overlay_.setMap(null); } if (this.maps_ && this.map_ && this.props.shouldUnregisterMapOnUnmount) { // fix google, as otherwise listeners works even without map this.map_.setOptions({ scrollwheel: false }); this.maps_.event.clearInstanceListeners(this.map_); } if (this.props.shouldUnregisterMapOnUnmount) { this.map_ = null; this.maps_ = null; } this.markersDispatcher_.dispose(); this.resetSizeOnIdle_ = false; if (this.props.shouldUnregisterMapOnUnmount) { 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 = (minZoom) => { if (!isEmpty(minZoom)) { return minZoom; } return this._getMinZoom(); }; _mapDomResizeCallback = () => { this.resetSizeOnIdle_ = true; if (this.maps_) { const originalCenter = this.props.center || this.props.defaultCenter; const currentCenter = this.map_.getCenter(); this.maps_.event.trigger(this.map_, 'resize'); this.map_.setCenter( this.props.resetBoundsOnResize ? originalCenter : currentCenter ); } }; _setLayers = (layerTypes) => { layerTypes.forEach((layerType) => { this.layers_[layerType] = new this.maps_[layerType](); this.layers_[layerType].setMap(this.map_); }); }; _renderPortal = () => ( ); _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, this.props.heatmapLibrary) .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), }; // Start Heatmap if (this.props.heatmap.positions) { Object.assign(this, { heatmap: generateHeatmap(maps, this.props.heatmap), }); optionsHeatmap(this.heatmap, this.props.heatmap); } // End Heatmap // 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 = !isEmpty(this.props.draggable) && { draggable: this.props.draggable, }; const minZoom = this._computeMinZoom(options.minZoom); this.minZoom_ = minZoom; const preMapOptions = { ...defaultOptions, minZoom, ...options, ...propsOptions, }; this.defaultDraggableOption_ = !isEmpty(preMapOptions.draggable) ? preMapOptions.draggable : this.defaultDraggableOption_; const mapOptions = { ...preMapOptions, ...draggableOptions, }; mapOptions.minZoom = _checkMinZoom(mapOptions.minZoom, minZoom); const map = new maps.Map( ReactDOM.findDOMNode(this.googleMapDom_), mapOptions ); this.map_ = map; this.maps_ = maps; this._setLayers(this.props.layerTypes); // Parse `google.maps.version` to capture the major version number. const versionMatch = maps.version.match(/^3\.(\d+)\./); // The major version is the first (and only) captured group. const mapsVersion = versionMatch && Number(versionMatch[1]); // 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'); 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; if (this_.props.overlayViewDivStyle) { const { overlayViewDivStyle } = this_.props; if (typeof overlayViewDivStyle === 'object') { Object.keys(overlayViewDivStyle).forEach((property) => { div.style[property] = overlayViewDivStyle[property]; }); } } const panes = this.getPanes(); panes.overlayMouseTarget.appendChild(div); this_.geoService_.setMapCanvasProjection( maps, overlay.getProjection() ); if (!IS_REACT_16) { createPortal( this_, this_._renderPortal(), div, // remove prerendered markers () => this_.setState({ overlay: div }) ); } else { this_.setState({ overlay: div }); } }, onRemove() { const renderedOverlay = this_.state.overlay; if (renderedOverlay && !IS_REACT_16) { ReactDOM.unmountComponentAtNode(renderedOverlay); } this_.setState({ overlay: null }); }, draw() { this_.updateCounter_++; this_._onBoundsChanged(map, maps, !this_.props.debounced); if (!this_.googleApiLoadedCalled_) { this_._onGoogleApiLoaded({ map, maps, ref: this_.googleMapDom_ }); this_.googleApiLoadedCalled_ = true; } if (this_.mouse_) { const latLng = this_.geoService_.fromContainerPixelToLatLng( this_.mouse_ ); this_.mouse_.lat = latLng.lat; this_.mouse_.lng = latLng.lng; } this_._onChildMouseMove(); if (this_.markersDispatcher_) { this_.markersDispatcher_.emit('kON_CHANGE'); if (this_.fireMouseEventOnIdle_) { this_.markersDispatcher_.emit('kON_MOUSE_POSITION_CHANGE'); } } }, }); this.overlay_ = overlay; overlay.setMap(map); if (this.props.heatmap.positions) { this.heatmap.setMap(map); } if (this.props.onTilesLoaded) { maps.event.addListener(map, 'tilesloaded', () => { this_._onTilesLoaded(); }); } 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(map.zoom); } // If draw() is not called each frame during a zoom animation, // simulate it. if (mapsVersion < DRAW_CALLED_DURING_ANIMATION_VERSION) { 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(options.minZoom); if (currMinZoom !== this.minZoom_) { this.minZoom_ = currMinZoom; map.setOptions({ minZoom: currMinZoom }); } this.resetSizeOnIdle_ = false; } if (this_.zoomAnimationInProgress_) { this_.zoomAnimationInProgress_ = false; this_._onZoomAnimationEnd(map.zoom); } this_.updateCounter_++; this_._onBoundsChanged(map, maps); this_.dragTime_ = 0; if (this_.markersDispatcher_) { this_.markersDispatcher_.emit('kON_CHANGE'); } }); maps.event.addListener(map, 'mouseover', () => { // has advantage over div MouseLeave this_.mouseInMap_ = true; }); // an alternative way to know the mouse is back within the map // This would not fire when clicking/interacting with google maps // own on-map countrols+markers. This handles an edge case for touch devices // + 'draggable:false' custom option. See #332 for more details. maps.event.addListener(map, 'click', () => { 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(map); }); maps.event.addListener(map, 'dragend', () => { // 'dragend' fires on mouse release. // 'idle' listener waits until drag inertia ends before firing `onDragEnd` const idleListener = maps.event.addListener(map, 'idle', () => { maps.event.removeListener(idleListener); this_._onDragEnd(map); }); }); // user choosing satellite vs roads, etc maps.event.addListener(map, 'maptypeid_changed', () => { this_._onMapTypeIdChange(map.getMapTypeId()); }); }) .catch((e) => { // notify callback of load failure this._onGoogleApiLoaded({ map: null, maps: null, ref: this.googleMapDom_, }); 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); _onDragEnd = (...args) => this.props.onDragEnd && this.props.onDragEnd(...args); _onMapTypeIdChange = (...args) => this.props.onMapTypeIdChange && this.props.onMapTypeIdChange(...args); _onZoomAnimationStart = (...args) => this.props.onZoomAnimationStart && this.props.onZoomAnimationStart(...args); _onZoomAnimationEnd = (...args) => this.props.onZoomAnimationEnd && this.props.onZoomAnimationEnd(...args); _onTilesLoaded = () => this.props.onTilesLoaded && this.props.onTilesLoaded(); _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; if (isFullScreen()) { this.geoService_.setViewSize(window.innerWidth, window.innerHeight); } else { const mapDom = ReactDOM.findDOMNode(this.googleMapDom_); 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_.fromContainerPixelToLatLng(this.mouse_); 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 this.zoomControlClickTime_ = new Date().getTime(); } }; _onKeyDownCapture = () => { if (detectBrowser().isChrome) { this.zoomControlClickTime_ = new Date().getTime(); } }; _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], }, sw: { lat: bounds[4], lng: bounds[5], }, ne: { lat: bounds[6], lng: bounds[7], }, }, marginBounds: { nw: { lat: marginBounds[0], lng: marginBounds[1], }, se: { lat: marginBounds[2], lng: marginBounds[3], }, sw: { lat: marginBounds[4], lng: marginBounds[5], }, ne: { lat: marginBounds[6], lng: marginBounds[7], }, }, size: this.geoService_.hasSize() ? { width: this.geoService_.getWidth(), height: this.geoService_.getHeight(), } : { width: 0, height: 0, }, }); } this.prevBounds_ = bounds; } } } }; _registerChild = (ref) => { this.googleMapDom_ = ref; }; render() { const overlay = this.state.overlay; const mapMarkerPrerender = !overlay ? ( ) : null; return (
{IS_REACT_16 && overlay && createPortal(this._renderPortal(), overlay)} {/* render markers before map load done */} {mapMarkerPrerender}
); } } export default GoogleMap;