aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAubrey-Li <63608597+Aubrey-Li@users.noreply.github.com>2021-07-17 12:58:57 -0700
committerAubrey-Li <63608597+Aubrey-Li@users.noreply.github.com>2021-07-17 12:58:57 -0700
commitd5c261f306a45fda46e948b9db001874a2d9a0ae (patch)
treeb9299d4f67798864df48b5b98831ee2499c2ed8a /src
parent992b5ca20414c28eba255cf319eb2b762cb69933 (diff)
add MapBox
Diffstat (limited to 'src')
-rw-r--r--src/client/views/nodes/MapBox/MapBox.scss32
-rw-r--r--src/client/views/nodes/MapBox/MapBox.tsx374
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