import { Autocomplete, GoogleMap, GoogleMapProps, InfoWindow, Marker } from '@react-google-maps/api'; import { action, computed, IReactionDisposer, observable, ObservableMap } from 'mobx'; import { observer } from "mobx-react"; import * as React from "react"; import { DataSym, Doc, DocListCast, FieldsSym, WidthSym } from '../../../../fields/Doc'; import { documentSchema } from '../../../../fields/documentSchemas'; import { makeInterface } from '../../../../fields/Schema'; import { NumCast, StrCast } from '../../../../fields/Types'; import { emptyFunction, setupMoveUpEvents } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; import { DragManager } from '../../../util/DragManager'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../../DocComponent'; import { AnchorMenu } from '../../pdf/AnchorMenu'; import { SidebarAnnos } from '../../SidebarAnnos'; import { StyleProp } from '../../StyleProvider'; import { FieldView, FieldViewProps } from '../FieldView'; import "./MapBox.scss"; import { MapMarker } from './MapMarker'; import { DocumentType } from '../../../documents/DocumentTypes'; import { identity } from 'lodash'; import { Id } from '../../../../fields/FieldSymbols'; import { Colors } from '../../global/globalEnums'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; type MapDocument = makeInterface<[typeof documentSchema]>; const MapDocument = makeInterface(documentSchema); export type Coordinates = { lat: number, lng: number, } export type LocationData = { id: string; pos: Coordinates; }; const mapContainerStyle = { height: '100%', }; const defaultCenter = { lat: 38.685, lng: -115.234, }; const mapOptions = { fullscreenControl: false, } const drawingManager = new google.maps.drawing.DrawingManager({ drawingControl: true, drawingControlOptions: { position: google.maps.ControlPosition.TOP_RIGHT, drawingModes: [ google.maps.drawing.OverlayType.MARKER, // currently we are not supporting the following drawing mode on map, a thought for future development // google.maps.drawing.OverlayType.CIRCLE, // google.maps.drawing.OverlayType.POLYLINE, ], }, }); const options = { fields: ["formatted_address", "geometry", "name"], // note: level of details is charged by item per retrieval, not recommended to return all fields strictBounds: false, types: ["establishment"], // type pf places, subject of change according to user need } as google.maps.places.AutocompleteOptions; @observer export class MapBox extends ViewBoxAnnotatableComponent, MapDocument>(MapDocument) { private _dropDisposer?: DragManager.DragDropDisposer; private _disposers: { [name: string]: IReactionDisposer } = {}; public static LayoutString(fieldKey: string) { return FieldView.LayoutString(MapBox, fieldKey); } @observable private _map: google.maps.Map = null as unknown as google.maps.Map; @observable private selectedPlace: MapMarker | undefined; @observable private markerMap: { [id: string]: google.maps.Marker } = {}; @observable private center = navigator.geolocation ? navigator.geolocation.getCurrentPosition : defaultCenter; @observable private zoom = 2.5; @observable private infoWindowOpen = false; @observable private bounds = new window.google.maps.LatLngBounds(); @observable private inputRef = React.createRef(); @observable private searchMarkers: google.maps.Marker[] = []; @observable private searchBox = new window.google.maps.places.Autocomplete(this.inputRef.current!, options); @observable private _savedAnnotations = new ObservableMap(); @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); }; @computed get allMapMarkers() { return DocListCast(this.dataDoc[this.annotationKey]); }; @observable private allMarkers: Doc[] = []; //TODO: change all markers to a filter function to change @observable _showSidebar = false; @computed get SidebarShown() { return this._showSidebar || this.layoutDoc._showSidebar ? true : false; } static _canAnnotate = true; static _hadSelection: boolean = false; private _sidebarRef = React.createRef(); private _ref: React.RefObject = React.createRef(); constructor(props: any) { super(props); } @action private setSearchBox = (searchBox: any) => { this.searchBox = searchBox; } // iterate allMarkers to size, center, and zoom map to contain all markers private fitBounds = (map: google.maps.Map) => { console.log('map bound is:' + this.bounds); this.allMarkers.map(place => { this.bounds.extend({ lat: NumCast(place.lat), lng: NumCast(place.lng) }); return place._markerId; }); map.fitBounds(this.bounds) } private hasGeolocation = (doc: Doc) => { return doc.type === DocumentType.IMG } /** * A function that examines allMapMarkers docs in the map node and form MapMarkers */ private fillMarkers = () => { this.allMapMarkers?.forEach(doc => { // search for if the map marker exists, else create marker if (doc.lat !== undefined && doc.lng !== undefined) { const marker = Docs.Create.MapMarkerDocument(NumCast(doc.lat), NumCast(doc.lng), [doc], {}) this.allMarkers.push(marker) } }) } // TODO: things to ask & think about when designing // 1. All markers are stored in allMarkers[], when adding a new marker (from a button, ideally not using drawManager), // the new marker will be stored in allMarkers[] // currently markerloadhandler only gets called when the map is reloaded, but we want it to be update on the GUI in real time // TODO ** core issue --> real time updates ** /** * store a reference to google map instance * setup the drawing manager on the top right corner of map * fit map bounds to contain all markers * @param map */ @action private loadHandler = (map: google.maps.Map) => { this._map = map; drawingManager.setMap(map); if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( (position: GeolocationPosition) => { const pos = { lat: position.coords.latitude, lng: position.coords.longitude, }; this._map.setCenter(pos); } ); } else { alert("Your geolocation is not supported by browser.") } this.fitBounds(map); this.fillMarkers(); // this._map.addListener(drawingManager, 'markercomplete', this.addMarker) } //TODO: Is this a valid way for adding marker from drawing manager..? If so, how do we update the allMarkers & render so info window appears @action private addMarker = (marker: google.maps.Marker) => { const markerPosition = marker.getPosition(); const newMapMarker = Docs.Create.MapMarkerDocument(NumCast(markerPosition?.lat()), NumCast(markerPosition?.lng()), [], {}) this.allMarkers.push(newMapMarker) } @action private markerLoadHandler = (marker: google.maps.Marker, place: Doc) => { place[Id] ? this.markerMap[place[Id]] = marker : null; } @action private markerClickHandler = (e: MouseEvent, place: any) => { // set which place was clicked this.selectedPlace = place; console.log(this.selectedPlace); // used so clicking a second marker works if (this.infoWindowOpen) { this.infoWindowOpen = false; console.log("closeinfowindow") } this.infoWindowOpen = true; console.log("open infowindow") } /** * Called when dragging documents into map sidebar * @param doc * @param sidebarKey * @returns */ sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { if (!this.layoutDoc._showSidebar) this.toggleSidebar(); const docs = doc instanceof Doc ? [doc] : doc docs.forEach(doc => { if (doc.lat !== undefined && doc.lng !== undefined) { const marker = Docs.Create.MapMarkerDocument(NumCast(doc.lat), NumCast(doc.lng), [], {}) this.addDocument(marker, this.annotationKey) } }) //add to annotation list return this.addDocument(doc, sidebarKey); // add to sidebar list } /** * What does this do exactly? How to operate on sidebar? * @param e */ sidebarBtnDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, (e, down, delta) => { const localDelta = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformDirection(delta[0], delta[1]); const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); const ratio = (curNativeWidth + localDelta[0] / (this.props.scaling?.() || 1)) / nativeWidth; if (ratio >= 1) { this.layoutDoc.nativeWidth = nativeWidth * ratio; this.layoutDoc._width = this.layoutDoc[WidthSym]() + localDelta[0]; this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth; } return false; }, emptyFunction, this.toggleSidebar); } sidebarWidth = () => Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100 * this.props.PanelWidth(); @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._sidebarWidthPercent, "0%"); } @computed get sidebarColor() { return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "#e4e4e4")); } /** * function that reads the place inputed from searchbox, then zoom in on the location that's been autocompleted; * add a customized temporary marker on the map */ @action private handlePlaceChanged = () => { console.log(this.searchBox); const place = this.searchBox.getPlace(); if (!place.geometry || !place.geometry.location) { // user entered the name of a place that wasn't suggested & pressed the enter key, or place details request failed window.alert("No details available for input: '" + place.name + "'"); return; } // zoom in on the location of the search result if (place.geometry.viewport) { console.log(this._map); this._map.fitBounds(place.geometry.viewport); } else { console.log(this._map); this._map.setCenter(place.geometry.location); this._map.setZoom(17); } // customize icon => customized icon for the nature of the location selected const icon = { url: place.icon as string, size: new google.maps.Size(71, 71), origin: new google.maps.Point(0, 0), anchor: new google.maps.Point(17, 34), scaledSize: new google.maps.Size(25, 25), }; // put temporary cutomized marker on searched location this.searchMarkers.forEach((marker) => { marker.setMap(null); }); this.searchMarkers = []; this.searchMarkers.push( new window.google.maps.Marker({ map: this._map, icon, title: place.name, position: place.geometry.location, }) ) } @action private handleInfoWindowClose = () => { if (this.infoWindowOpen) { this.infoWindowOpen = false; } this.infoWindowOpen = false; this.selectedPlace = undefined; } public get SidebarKey() { return this.fieldKey + "-sidebar"; } @computed get sidebarHandle() { const annotated = DocListCast(this.dataDoc[this.SidebarKey]).filter(d => d?.author).length; //&& !this.isContentActive() return (!annotated) ? (null) :
; } @action toggleSidebar = () => { const prevWidth = this.sidebarWidth(); this.layoutDoc._showSidebar = ((this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, "0%") === "0%" ? "50%" : "0%")) !== "0%"; this.layoutDoc._width = this.layoutDoc._showSidebar ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth); } sidebarDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), false); } sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { const bounds = this._ref.current!.getBoundingClientRect(); this.layoutDoc._sidebarWidthPercent = "" + 100 * Math.max(0, (1 - (e.clientX - bounds.left) / bounds.width)) + "%"; this.layoutDoc._showSidebar = this.layoutDoc._sidebarWidthPercent !== "0%"; e.preventDefault(); return false; } // TODO what's the difference between savedAnnotations & allMapMarkers? getAnchor = () => { const anchor = AnchorMenu.Instance?.GetAnchor(this._savedAnnotations) ?? this.rootDoc // if anchormenu pops up else return rootDoc (map) // Docs.Create.MapMarkerDocument(this.allMapMarkers, { // title: StrCast(this.rootDoc.title + " " + this.layoutDoc._scrollTop), // annotationOn: this.rootDoc, // y: NumCast(this.layoutDoc._scrollTop), // }); // this.addDocument(anchor); return anchor; } private saveMarkerInfo = () => { } // create marker prop --> func that private renderMarkers = () => { return this.allMarkers.map(place => ( this.markerLoadHandler(marker, place)} onClick={e => this.markerClickHandler(e, place)} /> )) } private renderInfoWindow = () => { return this.infoWindowOpen && this.selectedPlace && (
{/* {// TODO need to figure out how to render these childDocs of the MapMarker in InfoWindow marker.childDocs} */}




) } render() { return
{/* // {/* */}
e.stopPropagation()} onPointerDown={e => (e.button === 0 && !e.ctrlKey) && e.stopPropagation()} style={{ width: `calc(100% - ${this.sidebarWidthPercent})` }}> this.loadHandler(map)} options={mapOptions} > {this.renderMarkers()} {this.renderInfoWindow()}
{/* {/*
*/}
{this.sidebarHandle}
; } }