/** * Created by rburson on 4/27/16. */ import * as React from 'react' import {GoogleMap, Marker, InfoWindow, OverlayView, DirectionsRenderer, withGoogleMap} from 'react-google-maps' import * as SearchBoxMod from 'react-google-maps/lib/places/SearchBox' const SearchBox = SearchBoxMod.default; import { CvState, CvProps, CvBaseMixin, CvNavigationResult, CvEvent, CvContext, CvValueListener, CvStateChangeResult, CvQueryPaneCallback, CvQueryPane, CvValueAdapter, CvActionFiredResult, CvEventType, CvMessage, CvMessageType, CvValueProvider, CvActionHandlerParams, CvActionBase } from 'catreact' import { CvDropdownMenu } from './catreact-html' import {FormContext, MapContext, MapDef, ObjUtil, EntityRec, ArrayUtil, Log, DialogException} from 'catavolt-sdk' export interface CvMapPanelState extends CvState { } export interface CvMapPanelProps extends CvProps { paneRef?:number; formContext?:FormContext; mapContext?:MapContext; navigationListeners?:Array<(event:CvEvent)=>void>; selectionListener?:CvValueListener>; stateChangeListeners?:Array<(event:CvEvent)=>void>; actionListeners?:Array<(event:CvEvent)=>void> navTarget?:string; containerProps?:{} actionProvider?:CvValueProvider; } /* *************************************************** * Render a Map *************************************************** */ export var CvMapPanel = React.createClass({ mixins: [CvBaseMixin], componentDidMount: function () { const event:CvEvent = {type:CvEventType.MESSAGE, eventObj:{message: 'Map Tip: Click on multiple markers to see route and distance. Right-click for a context menu.', type:CvMessageType.INFO}} this.eventRegistry().publish(event, false); CvActionBase._publishActionStarted('#loadMap', this.props.mapContext, true, this.props.actionListeners, this.eventRegistry()); }, getDefaultProps: function () { return { paneRef: null, formContext: null, mapContext: null, navigationListeners: [], selectionListener: null, stateChangeListeners: [], actionListeners: [], navTarget: null, containerProps: null, actionProvider: null } }, getInitialState: function () { return {currentSelection: null, previousSelection: null, openMarker: null, openMenu: null, directions: null} }, render: function () { const queryPaneProps = { paneRef: this.props.paneRef, formContext: this.props.formContext, queryContext: this.props.mapContext, stateChangeListeners: this.props.stateChangeListeners, actionListeners: this.props.actionListeners, actionProvider: this.props.actionProvider } return { const mapContext:MapContext = cvContext.scopeCtx.scopeObj; const passthroughProps = { paneRef: this.props.paneRef, formContext:this.props.formContext, mapContext: mapContext, lastRefreshTime: mapContext.lastRefreshTime, navigationListeners:this.props.navigationListeners, selectionListener:this.props.selectionListener, stateChangeListeners:this.props.stateChangeListeners, actionListeners:this.props.actionListeners, navTarget:this.props.navTarget, actionProvider:this.props.actionProvider } const mapContainerProps = ObjUtil.addAllProps(this.props.containerProps, {className: 'cv-component-container cv-map-container'}) return ( } mapElement={
} {...passthroughProps} /> ); }}/> } }); export interface CvMapState extends CvState { openMarker:CvMarker; currentSelection:CvMarker; previousSelection:CvMarker; openMenu:{}; directions:{}; persistentMarkers:Array; searchMarkers:Array; userMarkers:Array; } export interface CvMapProps extends CvProps { paneRef?:number; formContext?:FormContext; mapContext?:MapContext; lastRefreshTime?:Date; navigationListeners?:Array<(event:CvEvent)=>void>; selectionListener?:CvValueListener>; stateChangeListeners?:Array<(event:CvEvent)=>void>; actionListeners?:Array<(event:CvEvent)=>void> navTarget?:string; actionProvider?:CvValueProvider; } export interface CvMarker { id:string; lat:number; lng:number; desc:string; imgUrl?: string; tipText?:string; imagePlc?:string; draggable?:boolean; } const CvMapPanel_INPUT_STYLE = { boxSizing: `border-box`, MozBoxSizing: `border-box`, border: `1px solid transparent`, width: `240px`, height: `32px`, marginTop: `27px`, padding: `0 12px`, borderRadius: `1px`, boxShadow: `0 2px 6px rgba(0, 0, 0, 0.3)`, fontSize: `14px`, outline: `none`, textOverflow: `ellipses`, }; const CvMap = React.createClass({ mixins: [CvBaseMixin], componentWillMount: function() { this.setState({persistentMarkers: this._generatePersistentMarkers()}); }, componentWillReceiveProps: function(nextProps, nextState) { if(nextProps.lastRefreshTime && (nextProps.lastRefreshTime.getTime() > this.props.lastRefreshTime.getTime())){ this.setState({persistentMarkers: this._generatePersistentMarkers()}); } }, getDefaultProps: function () { return { paneRef: null, formContext: null, mapContext: null, lastRefreshTime: null, navigationListeners: [], selectionListener: null, stateChangeListeners: [], actionListeners: [], navTarget: null, actionProvider: null } }, getInitialState: function () { return {currentSelection: null, previousSelection: null, openMarker: null, openMenu: null, directions: null, persistentMarkers: [], searchMarkers:[], userMarkers:[]} }, render: function () { const {currentSelection, previousSelection, directions, openMenu, openMarker} = this.state; const mapContext = this.props.mapContext; const markers:Array = this._getAllMarkers(); return ( {if(map){ this._initMap(map, markers)}}}> this._searchBox = ref} /> {directions ? : null} {markers.map((marker:CvMarker, index:number)=>{ const menuDef = (mapContext.menuDefs && mapContext.menuDefs.length > 0) ? mapContext.menuDefs[0].findContextMenuDef() : null; const selectionAdapter:CvValueAdapter> = new CvValueAdapter>(); selectionAdapter.getDelegateValueListener()([marker.id]); /* Note: We have to explicitly pass every prop to the CvDropdownMenu component that might normally be found in the context because the 'GoogleMap' react library interferes with the context */ return ( {openMarker && openMarker.id === marker.id ? this._getInfoDisplay(directions, marker, currentSelection) : null} {openMenu && openMenu.id === marker.id ? } eventRegistry={this.eventRegistry()}/> : null} ); })} ); }, _actionFired: function (marker:any, event:CvEvent) { }, _getAllMarkers: function():Array { return [...this.state.persistentMarkers, ...this.state.searchMarkers, ...this.state.userMarkers]; }, /* Use info window to display directions */ _getInfoDisplay: function(directions, marker:CvMarker, targetMarker:CvMarker) { if(!directions) { return
{marker.desc}
} else { if(marker.id == targetMarker.id) { const leg = directions.routes[0].legs[0]; return
} } return null; }, /* Create the markers that are included in the ListContext */ _generatePersistentMarkers: function():Array { const mapContext = this.props.mapContext; const mapDef:MapDef = mapContext.mapDef; const records:Array = ArrayUtil.copy(mapContext.scroller.buffer); const markers:Array = records.map((record:EntityRec)=>{ return { id: record.objectId, lat: record.propAtName(mapDef.latitudePropName).value, lng: record.propAtName(mapDef.longitudePropName).value, desc: record.propAtName(mapDef.descriptionPropName).value, imgUrl: record.imageName, tipText: record.tipText, imagePlc: record.imagePlacement } }); return markers; }, _getPixelPositionOffset: function(width, height) { return { x: -(width / 2), y: -(height / 2) }; }, /* Update the position and info of the User-placed Marker, when dragged */ _handleDragEnd: function(marker:CvMarker, e) { const currentUserMarkers = this.state.userMarkers; currentUserMarkers.splice(currentUserMarkers.indexOf(marker), 1); const nextMarker = ObjUtil.addAllProps(marker, {}); const lat = e.latLng.lat(); const lng = e.latLng.lng(); nextMarker.lat = lat; nextMarker.lng = lng; nextMarker.desc = lat + ', ' + lng; currentUserMarkers.push(nextMarker); this._reverseGeolocate(lat, lng, ((address)=>{ nextMarker.desc = address; this.setState({userMarkers: currentUserMarkers}); })); }, /* Clicking on an empty area of the map should reset selected markers, directions, etc. */ _handleMapClick: function(e) { if(!this.state.openMenu) { this.setState({ openMarker: null, directions: null, currentSelection: null, previousSelection: null }); } }, /* Place a User Marker, where double-clicked */ _handleMapDoubleClick: function(e) { const lat = e.latLng.lat(); const lng = e.latLng.lng(); const userMarkers:Array = [...this.state.userMarkers]; const marker = { id: 'user_' + userMarkers.length, lat: lat, lng: lng, desc: 'Dropped Pin', tipText: lat + ', ' + lng, draggable: true } this._reverseGeolocate(lat, lng, (address=>marker.desc = address)); userMarkers.push(marker); this.setState({userMarkers: userMarkers}); }, /* Remove a User Marker when double-clicked */ _handleMarkerDoubleClick: function(marker:CvMarker, e) { const currentUserMarkers = this.state.userMarkers; currentUserMarkers.splice(currentUserMarkers.indexOf(marker), 1); this.setState({ userMarkers: currentUserMarkers, openMarker: null, directions: null, currentSelection: null, previousSelection: null }); }, /* Show marker info when moused over */ _handleMarkerOver: function (marker, e) { if(!this.state.directions) { this.setState({ openMarker: marker, openMenu: null }); } }, /* When marker is clicked, make it the current selection and check for a previously clicked marker from which to route directions */ _handleMarkerClick: function (marker, e) { if (this.props.selectionListener) this.props.selectionListener([marker.id]); const previousSelection = this.state.currentSelection; const currentSelection = marker; if(previousSelection && currentSelection.id !== previousSelection.id) { const ds = new google.maps.DirectionsService(); ds.route({ origin: new google.maps.LatLng(previousSelection.lat, previousSelection.lng), destination: new google.maps.LatLng(currentSelection.lat, currentSelection.lng), travelMode: google.maps.TravelMode.DRIVING, provideRouteAlternatives: true }, (result, status) => { if (status === google.maps.DirectionsStatus.OK) { this.setState({ directions: result, currentSelection: currentSelection, previousSelection: previousSelection, openMarker: marker, openMenu: null }); } else { const event:CvEvent = {type:CvEventType.MESSAGE, eventObj:{message: `error fetching directions ${ result }`, messageObj:new DialogException('', 'Error retrieving directions'), type:CvMessageType.ERROR}} this.eventRegistry().publish(event, false); } }); } else { this.setState({ currentSelection: currentSelection, previousSelection: previousSelection, openMenu: null, directions: null }); } }, /* Show context menu when a Persistent Marker is right-clicked */ _handleMarkerRightClick: function(marker, e) { if (this.props.selectionListener) this.props.selectionListener([marker.id]); this.setState({ previousSelection: this.state.currentSelection, currentSelection: marker, openMenu: marker }); }, /* Close the info window */ _handleInfoClose: function (marker, e) { this.setState({openMarker: null}); }, /* Initialize the map and set bounds */ _initMap: function (map:any, latLng:Array<{lat; lng;}>) { if (!this._gMap && latLng && latLng.length > 0) { this._gMap = map; this._setBounds(map, latLng); CvActionBase._publishActionFinished('#loadMap', this.props.mapContext, this.props.actionListeners, this.eventRegistry()); } }, _setBounds: function(map:any, latLng:Array<{lat; lng;}>) { const latLngBounds = new google.maps.LatLngBounds(); for (var i = 0; i < latLng.length; i++) { latLngBounds.extend(new google.maps.LatLng(latLng[i].lat, latLng[i].lng)); } map.fitBounds(latLngBounds); }, _geoLocate: function(callback:(pos:{lat; lng;})=>void) { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(function(position) { var pos = { lat: position.coords.latitude, lng: position.coords.longitude }; callback(pos); }, function() { const event:CvEvent = {type:CvEventType.MESSAGE, eventObj:{message: 'Geolocation failed', messageObj:new DialogException('', 'Geolocation failed'), type:CvMessageType.ERROR}} this.eventRegistry().publish(event, false); }); } else { const event:CvEvent = {type:CvEventType.MESSAGE, eventObj:{message: 'Geolocation not supported (or disabled)', messageObj:new DialogException('', 'Geolocation not supported (or disabled)'), type:CvMessageType.ERROR}} this.eventRegistry().publish(event, false); } }, /* Handle search results */ _onPlacesChanged: function() { const markers = []; this._searchBox.getPlaces().forEach((place)=>{ markers.push({ id: place.place_id, lat: place.geometry.location.lat(), lng: place.geometry.location.lng(), desc: place.formatted_address, imgUrl: place.icon, tipText: place.name, }); }); this._setBounds(this._gMap, [...markers, ...this.state.userMarkers, ...this.state.persistentMarkers]); this.setState({searchMarkers: markers}); }, _reverseGeolocate: function(lat, lng, callback:(address:string)=>void) { var geocoder = new google.maps.Geocoder; geocoder.geocode({'location': {lat: lat, lng: lng}}, (results, status) => { if (status == google.maps.GeocoderStatus.OK) { if (results[0]) { callback(results[0].formatted_address) } } else { const event:CvEvent = {type:CvEventType.MESSAGE, eventObj:{message: 'Reverse Geolocation failed', messageObj:new DialogException('', 'Reverse Geolocation failed'), type:CvMessageType.ERROR}} this.eventRegistry().publish(event, false); } }); } }); const CvWrappedMap = withGoogleMap(props => ());