diff options
author | Aubrey-Li <63608597+Aubrey-Li@users.noreply.github.com> | 2021-07-17 12:58:57 -0700 |
---|---|---|
committer | Aubrey-Li <63608597+Aubrey-Li@users.noreply.github.com> | 2021-07-17 12:58:57 -0700 |
commit | d5c261f306a45fda46e948b9db001874a2d9a0ae (patch) | |
tree | b9299d4f67798864df48b5b98831ee2499c2ed8a /src | |
parent | 992b5ca20414c28eba255cf319eb2b762cb69933 (diff) |
add MapBox
Diffstat (limited to 'src')
-rw-r--r-- | src/client/views/nodes/MapBox/MapBox.scss | 32 | ||||
-rw-r--r-- | src/client/views/nodes/MapBox/MapBox.tsx | 374 |
2 files changed, 406 insertions, 0 deletions
diff --git a/src/client/views/nodes/MapBox/MapBox.scss b/src/client/views/nodes/MapBox/MapBox.scss new file mode 100644 index 000000000..863907aaf --- /dev/null +++ b/src/client/views/nodes/MapBox/MapBox.scss @@ -0,0 +1,32 @@ +.MapBox { + width: 100%; + height: 100%; + overflow: hidden; + + .MapBox-contents { + width: 100%; + height: 100%; + overflow: hidden; + > div { + position: unset !important; // when the sidebar filter flys out, this prevents the map from extending outside the document box + } + + .map-wrapper { + .searchbox { + box-sizing: border-box; + border: 1px solid transparent; + width: 240px; + height: 32px; + padding: 0 12px; + border-radius: 3px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); + font-size: 14px; + outline: none; + text-overflow: ellipses; + position: absolute; + left: 50%; + margin-left: -120px; + } + } + } +} diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx new file mode 100644 index 000000000..aa220c4da --- /dev/null +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -0,0 +1,374 @@ +import { Autocomplete, GoogleMap, GoogleMapProps, InfoBox, 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 { NumCast } from '../../../../fields/Types'; +import { setupMoveUpEvents, emptyFunction } from '../../../../Utils'; +import { DragManager } from '../../../util/DragManager'; +import { undoBatch } from '../../../util/UndoManager'; +import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../../DocComponent'; +import { SidebarAnnos } from '../../SidebarAnnos'; +import { FieldView, FieldViewProps } from '../FieldView'; +import "./MapBox.scss" + +type MapDocument = makeInterface<[typeof documentSchema]>; +const MapDocument = makeInterface(documentSchema); + +export type LocationData = { + id?: number; + pos?: { lat: number, lng: number }; +}; + +const mapContainerStyle = { + height: '100%', +}; + +const defaultCenter = { + lat: 38.685, + lng: -115.234, +}; + +@observer +export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps & Partial<GoogleMapProps>, MapDocument>(MapDocument) { + private _dropDisposer?: DragManager.DragDropDisposer; + private _disposers: { [name: string]: IReactionDisposer } = {}; + private _sidebarRef = React.createRef<SidebarAnnos>(); + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(MapBox, fieldKey); } + + @observable private _map = null as unknown as google.maps.Map; + @observable private selectedPlace: LocationData = null as any; + @observable private markerMap = {}; + @observable private center = defaultCenter; + @observable private zoom = 2.5; + @observable private clickedLatLng = null; + @observable private infoWindowOpen = false; + @observable private bounds = new window.google.maps.LatLngBounds(); + @observable private inputRef = React.createRef<HTMLInputElement>(); + @observable private buttonRef = React.createRef<HTMLDivElement>(); + @observable private searchMarkers: google.maps.Marker[] = []; + + private 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; + + @observable private searchBox = new window.google.maps.places.Autocomplete(this.inputRef.current!, this.options); + + @observable private myPlaces = [ + { id: 1, pos: { lat: 39.09366509575983, lng: -94.58751660204751 } }, + { id: 2, pos: { lat: 41.82399, lng: -71.41283 } }, + { id: 3, pos: { lat: 47.606214, lng: -122.33207 } }, + ]; + + @action + private setSearchBox = (searchBox: any) => { + this.searchBox = searchBox; + } + + @action + private setButton = (button: any) => { + this.buttonRef = button; + this._map.controls[google.maps.ControlPosition.TOP_RIGHT].push(this.buttonRef.current!) + } + + // iterate myPlaces to size, center, and zoom map to contain all markers + private fitBounds = (map: google.maps.Map) => { + console.log('map bound is:' + this.bounds); + this.myPlaces.map(place => { + this.bounds.extend(place.pos); + return place.id; + }); + 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; + this.fitBounds(map); + + // //add a custom control for button add marker + // //TODO: why this doesn't work + // const google = window.google; + // console.log("google window: " + google) + // const controlButtonDiv = document.createElement('div'); + // ReactDOM.render(<button onClick={() => console.log('click me to add marker!')}>Add Marker</button>, controlButtonDiv); + // map.controls[google.maps.ControlPosition.TOP_RIGHT].push(controlButtonDiv); + } + + @action + private markerClickHandler = (e: MouseEvent, place: LocationData) => { + // set which place was clicked + this.selectedPlace = place + + console.log(this.selectedPlace.id); + console.log(this.selectedPlace.pos); + + // used so clicking a second marker works + if (this.infoWindowOpen) { + this.infoWindowOpen = false; + console.log("closeinfowindow") + } + else { + this.infoWindowOpen = true; + console.log("open infowindow") + } + } + + sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { + if (!this.layoutDoc._showSidebar) this.toggleSidebar(); + return this.addDocument(doc, sidebarKey); + } + 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 + @undoBatch + private handleDragMarker = (marker: any, place: LocationData) => { + // if (marker != null) { + // place = { + // id: place.id, + // position: { + // lat: marker.latLng.lat().toFixed(3), + // lng: marker.latLng.lng().toFixed(3) + // } + // } + + // console.log(place); + // console.log(this.myPlaces); + // } + } + + @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 (hard to tell from other pre-existing markers, probably won't implement + */ + // 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), + // }; + + this.searchMarkers.forEach((marker) => { + marker.setMap(null); + }); + this.searchMarkers = []; + this.searchMarkers.push( + new window.google.maps.Marker({ + map: this._map, + title: place.name, + position: place.geometry.location, + }) + ) + } + + // @computed get markerContent() { + // const allMarkers = this.childLayoutPairs + // const markerId = NumCast(this.layoutDoc._itemIndex); + // const selectedMarker = this.childLayoutPairs?.[markerId]; + // const position = { + // lat: NumCast(this.layoutDoc.lat), + // lng: NumCast(this.layoutDoc.lng) + // } + // return <> + // { + // allMarkers?.map(place => ( + // <Marker + // key={markerId} + // position={position} + // onClick={e => this.markerClickHandler(e, place.layout)} //?? + // draggable={true} + // onDragEnd={marker => this.handleDragMarker(marker, place.layout)} + // /> + // )) + // } + // {this.infoWindowOpen && selectedMarker && ( + // <InfoBox + // //anchor={selectedMarker} + // // onCloseClick={this.handleInfoWindowClose} + // position={position} + // // options={{ enableEventPropagation: true }} + // > + // <div style={{ backgroundColor: 'white', opacity: 0.75, padding: 12 }}> + // <div style={{ fontSize: 16 }}> + // {/* the linkmenu as the ones in other nodes */} + // <div> + // <a>a link to another node</a> + // <hr /> + // </div> + // <div> + // <a>a link to another node</a> + // <hr /> + // </div> + // <div> + // <a>a link to another node</a> + // <hr /> + // </div> + // <div> + // <button>New +</button> + // </div> + // </div> + // </div> + // </InfoBox> + // )} + + // </> + // } + + @action + private addMarker = (location: google.maps.LatLng | undefined, map: google.maps.Map) => { + new window.google.maps.Marker({ + position: location, + map: map + }); + } + + // @action + // private renderMarkerToMap = (marker: Doc) => { + // const id = StrCast(marker.id); + // const lat = NumCast(marker.lat); + // const lng = NumCast(marker.lng); + + // return <Marker + // key={id} + // position={{ lat: lat, lng: lng }} + // onClick={e => this.markerClickHandler(e, marker)} + // /> + // } + + render() { + const { Document, fieldKey, isContentActive: active } = this.props; + + return <div className="MapBox"> + + <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)} + > + <Autocomplete + onLoad={this.setSearchBox} + onPlaceChanged={this.handlePlaceChanged}> + <input ref={this.inputRef} className="searchbox" type="text" placeholder="Search anywhere:" /> + </Autocomplete> + + <div onLoad={this.setButton}> + <div ref={this.buttonRef} className="add-button-UI" > + <div className="add-button-Text">Add Marker</div> + </div> + </div> + + {this.myPlaces.map(place => ( + <Marker + key={place.id} + position={place.pos} + // onLoad={marker => this.markerLoadHandler(marker, place)} + onClick={e => this.markerClickHandler(e, place)} + draggable={true} + onDragEnd={marker => this.handleDragMarker(marker, place)} + /> + ))} + {this.infoWindowOpen && this.selectedPlace && ( + <InfoBox + // anchor={this.markerMap[this.selectedPlace.id]} + // onCloseClick={this.handleInfoWindowClose} + position={this.selectedPlace.pos} + // options={{ enableEventPropagation: true }} + > + <div style={{ backgroundColor: 'white', opacity: 0.75, padding: 12 }}> + <div style={{ fontSize: 16 }}> + <div> + <a>a link to another node</a> + <hr /> + </div> + <div> + <a>a link to another node</a> + <hr /> + </div> + <div> + <a>a link to another node</a> + <hr /> + </div> + <div> + <button>New +</button> + </div> + </div> + </div> + </InfoBox> + )} + </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 |