aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/MapBox/MapBox.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/MapBox/MapBox.tsx')
-rw-r--r--src/client/views/nodes/MapBox/MapBox.tsx354
1 files changed, 354 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..56203f3ae
--- /dev/null
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -0,0 +1,354 @@
+import { Autocomplete, GoogleMap, GoogleMapProps, InfoWindow, Marker } from '@react-google-maps/api';
+import { action, computed, IReactionDisposer, observable } from 'mobx';
+import { observer } from "mobx-react";
+import * as React from "react";
+import { Doc, DocListCast, 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 { DragManager } from '../../../util/DragManager';
+import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../../DocComponent';
+import { SidebarAnnos } from '../../SidebarAnnos';
+import { StyleProp } from '../../StyleProvider';
+import { FieldView, FieldViewProps } from '../FieldView';
+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 = 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<HTMLInputElement>();
+ @observable private searchMarkers: google.maps.Marker[] = [];
+ @observable private searchBox = new window.google.maps.places.Autocomplete(this.inputRef.current!, options);
+ @observable private childDocs: MapMarker[] = [];
+
+
+ @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<SidebarAnnos>();
+ private _ref: React.RefObject<HTMLDivElement> = React.createRef();
+
+ constructor(props: any) {
+ super(props);
+ }
+
+ @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);
+ 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);
+ }
+
+ @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);
+ }
+
+ 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")); }
+
+
+ @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
+ });
+ }
+
+ public get SidebarKey() { return this.fieldKey + "-sidebar"; }
+ @computed get sidebarHandle() {
+ const annotated = DocListCast(this.dataDoc[this.SidebarKey]).filter(d => d?.author).length;
+ return (!annotated && !this.isContentActive()) ? (null) : <div className="mapBox-sidebar-handle" onPointerDown={this.sidebarDown}
+ style={{
+ left: `max(0px, calc(100% - ${this.sidebarWidthPercent} ${this.sidebarWidth() ? "- 5px" : "- 10px"}))`,
+ background: this.props.styleProvider?.(this.rootDoc, this.props as any, StyleProp.WidgetColor + (annotated ? ":annotated" : ""))
+ }} />;
+ }
+ @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;
+ }
+
+ render() {
+ return <div className="mapBox" ref={this._ref}
+ style={{ pointerEvents: this.isContentActive() ? undefined : "none" }} >
+ {/* // {/* <LoadScript
+ // googleMapsApiKey={process.env.GOOGLE_MAPS!}
+ // libraries={['places', 'drawing']}
+ // > */}
+ <div className="mapBox-wrapper"
+ onWheel={e => e.stopPropagation()}
+ onPointerDown={e => (e.button === 0 && !e.ctrlKey) && e.stopPropagation()}
+ style={{ width: `calc(100% - ${this.sidebarWidthPercent})` }}>
+ <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 > */}
+ <div className="mapBox-sidebar"
+ style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
+ <SidebarAnnos ref={this._sidebarRef}
+ {...this.props}
+ fieldKey={this.annotationKey}
+ rootDoc={this.rootDoc}
+ layoutDoc={this.layoutDoc}
+ dataDoc={this.dataDoc}
+ showSidebar={this.SidebarShown}
+ nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
+ PanelWidth={this.sidebarWidth}
+ sidebarAddDocument={this.sidebarAddDocument}
+ moveDocument={this.moveDocument}
+ removeDocument={this.removeDocument}
+ isContentActive={this.isContentActive}
+ />
+ </div>
+ {this.sidebarHandle}
+ </div>;
+ }
+} \ No newline at end of file