diff options
Diffstat (limited to 'src/client/views/nodes/MapBox/MapBox.tsx')
-rw-r--r-- | src/client/views/nodes/MapBox/MapBox.tsx | 319 |
1 files changed, 319 insertions, 0 deletions
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx new file mode 100644 index 000000000..d52b91908 --- /dev/null +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -0,0 +1,319 @@ +import { Autocomplete, GoogleMap, GoogleMapProps, InfoBox, InfoWindow, Marker } from '@react-google-maps/api'; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx'; +import { observer } from "mobx-react"; +import * as React from "react"; +import { Doc, WidthSym } from '../../../../fields/Doc'; +import { documentSchema } from '../../../../fields/documentSchemas'; +import { makeInterface } from '../../../../fields/Schema'; +import { Cast, NumCast, StrCast } from '../../../../fields/Types'; +import { setupMoveUpEvents, emptyFunction } from '../../../../Utils'; +import { DragManager } from '../../../util/DragManager'; +import { undoBatch } from '../../../util/UndoManager'; +import { CollectionViewType } from '../../collections/CollectionView'; +import { TabDocView } from '../../collections/TabDocView'; +import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../../DocComponent'; +import { SidebarAnnos } from '../../SidebarAnnos'; +import { StyleProp } from '../../StyleProvider'; +import { FieldView, FieldViewProps } from '../FieldView'; +import { PresMovement } from '../PresBox'; +import "./MapBox.scss" +import { MapMarker } from './MapMarker'; + +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<ViewBoxAnnotatableProps & FieldViewProps & Partial<GoogleMapProps>, 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 = defaultCenter; + @observable private zoom = 2.5; + @observable private infoWindowOpen = false; + @observable private bounds = new window.google.maps.LatLngBounds(); + @observable private inputRef = React.createRef<HTMLInputElement>(); + @observable private searchMarkers: google.maps.Marker[] = []; + @observable private searchBox = new window.google.maps.places.Autocomplete(this.inputRef.current!, options); + @observable private childDocs: MapMarker[] = []; + + static _canAnnotate = true; + static _hadSelection: boolean = false; + private _sidebarRef = React.createRef<SidebarAnnos>(); + private _ref: React.RefObject<HTMLDivElement> = React.createRef(); + + + @action + private setSearchBox = (searchBox: any) => { + this.searchBox = searchBox; + } + + // iterate childDocs to size, center, and zoom map to contain all markers + private fitBounds = (map: google.maps.Map) => { + console.log('map bound is:' + this.bounds); + this.childDocs.map(place => { + this.bounds.extend(place._latlngLocation); + return place._markerId; + }); + map.fitBounds(this.bounds) + } + + // store a reference to google map instance; fit map bounds to contain all markers + @action + private loadHandler = (map: google.maps.Map) => { + this._map = map; + drawingManager.setMap(map); + this.fitBounds(map); + } + + @action + private markerLoadHandler = (marker: google.maps.Marker, place: MapMarker) => { + place._markerId ? this.markerMap[place._markerId] = 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(); + return this.addDocument(doc, sidebarKey); + } + + /** + * 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); + } + + toggleSidebar = action(() => { + const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); + const ratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? 250 : 0) + nativeWidth) / nativeWidth; + const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); + this.layoutDoc.nativeWidth = nativeWidth * ratio; + this.layoutDoc._width = this.layoutDoc[WidthSym]() * nativeWidth * ratio / curNativeWidth; + this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth; + }); + + sidebarWidth = () => { + !this.layoutDoc._showSidebar ? 0 : + (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / NumCast(this.layoutDoc.nativeWidth); + } + + @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; + } + + @action + private addMarker = (location: google.maps.LatLng | undefined, map: google.maps.Map) => { + new window.google.maps.Marker({ + position: location, + map: map + }); + } + + render() { + const { Document, fieldKey, isContentActive: active } = this.props; + + return <div className="MapBox" ref={this._ref}> + {/* HELLO WORLD! */} + <div className={"MapBox-contents"} + style={{ pointerEvents: active() ? undefined : "none", overflow: 'hidden' }} + onWheel={e => e.stopPropagation()} + onPointerDown={e => (e.button === 0 && !e.ctrlKey) && e.stopPropagation()} > + {/* // {/* <LoadScript + // googleMapsApiKey={process.env.GOOGLE_MAPS!} + // libraries={['places', 'drawing']} + // > */} + <div className="map-wrapper"> + <GoogleMap + mapContainerStyle={mapContainerStyle} + zoom={this.zoom} + center={this.center} + onLoad={map => this.loadHandler(map)} + options={mapOptions} + > + <Autocomplete + onLoad={this.setSearchBox} + onPlaceChanged={this.handlePlaceChanged}> + <input ref={this.inputRef} className="searchbox" type="text" placeholder="Search anywhere:" /> + </Autocomplete> + + {this.childDocs.map(place => ( + <Marker + key={place._markerId} + position={place._latlngLocation} + onLoad={marker => this.markerLoadHandler(marker, place)} + onClick={e => this.markerClickHandler(e, place)} + /> + ))} + {this.infoWindowOpen && this.selectedPlace && ( + <InfoWindow + anchor={this.markerMap[this.selectedPlace._markerId!]} + onCloseClick={this.handleInfoWindowClose} + > + <div style={{ backgroundColor: 'white', opacity: 0.75, padding: 12 }}> + <div style={{ fontSize: 16 }}> + <div> + <img src="http://placekitten.com/200/300" /> + <hr /> + <form> + <label>Title: </label><br /> + <input type="text" id="fname" name="fname"></input><br /> + <label>Desription: </label><br /> + <textarea style={{ height: 150 }} id="lname" name="lname" placeholder="Notes, a short description of this location, a brief comment, etc."></textarea> + </form> + <hr /> + <div> + <button>New link+</button> + </div> + </div> + </div> + </div> + </InfoWindow> + )} + </GoogleMap> + </div> + {/* {/* </LoadScript > */} + <SidebarAnnos ref={this._sidebarRef} + {...this.props} + fieldKey={this.annotationKey} + rootDoc={this.rootDoc} + layoutDoc={this.layoutDoc} + dataDoc={this.dataDoc} + sidebarAddDocument={this.sidebarAddDocument} + moveDocument={this.moveDocument} + removeDocument={this.removeDocument} + isContentActive={this.isContentActive} + /> + </div > + </div >; + } + +}
\ No newline at end of file |