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.tsx691
1 files changed, 691 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..7875060e2
--- /dev/null
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -0,0 +1,691 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Autocomplete, GoogleMap, GoogleMapProps, InfoWindow, LoadScript, 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 { Doc, DocListCast, Opt, WidthSym } from '../../../../fields/Doc';
+import { documentSchema } from '../../../../fields/documentSchemas';
+import { Id } from '../../../../fields/FieldSymbols';
+import { InkTool } from '../../../../fields/InkField';
+import { makeInterface } from '../../../../fields/Schema';
+import { NumCast, StrCast } from '../../../../fields/Types';
+import { TraceMobx } from '../../../../fields/util';
+import { emptyFunction, OmitKeys, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { Docs } from '../../../documents/Documents';
+import { CurrentUserUtils } from '../../../util/CurrentUserUtils';
+import { DragManager } from '../../../util/DragManager';
+import { SelectionManager } from '../../../util/SelectionManager';
+import { SnappingManager } from '../../../util/SnappingManager';
+import { undoBatch } from '../../../util/UndoManager';
+import { CollectionFreeFormView, MarqueeOptionsMenu } from '../../collections/collectionFreeForm';
+import { CollectionStackingView } from '../../collections/CollectionStackingView';
+import { CollectionViewType } from '../../collections/CollectionView';
+import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../../DocComponent';
+import { Colors } from '../../global/globalEnums';
+import { MarqueeAnnotator } from '../../MarqueeAnnotator';
+import { AnchorMenu } from '../../pdf/AnchorMenu';
+import { Annotation } from '../../pdf/Annotation';
+import { SidebarAnnos } from '../../SidebarAnnos';
+import { StyleProp } from '../../StyleProvider';
+import { FieldView, FieldViewProps } from '../FieldView';
+import * as dotenv from 'dotenv';
+import "./MapBox.scss";
+
+/**
+ * MapBox architecture:
+ * Main component: MapBox.tsx
+ * Supporting Components: SidebarAnnos, CollectionStackingView
+ *
+ * MapBox is a node that extends the ViewBoxAnnotatableComponent. Similar to PDFBox and WebBox, it supports interaction between sidebar content and document content.
+ * The main body of MapBox uses Google Maps API to allow location retrieval, adding map markers, pan and zoom, and open street view.
+ * Dash Document architecture is integrated with Maps API: When drag and dropping documents with ExifData (gps Latitude and Longitude information) available,
+ * sidebarAddDocument function checks if the document contains lat & lng information, if it does, then the document is added to both the sidebar and the infowindow (a pop up corresponding to a map marker--pin on map).
+ * The lat and lng field of the document is filled when importing (spec see ConvertDMSToDD method and processFileUpload method in Documents.ts).
+ * A map marker is considered a document that contains a collection with stacking view of documents, it has a lat, lng location, which is passed to Maps API's custom marker (red pin) to be rendered on the google maps
+ */
+
+// const _global = (window /* browser */ || global /* node */) as any;
+
+type MapDocument = makeInterface<[typeof documentSchema]>;
+const MapDocument = makeInterface(documentSchema);
+
+const mapContainerStyle = {
+ height: '100%',
+};
+
+const defaultCenter = {
+ lat: 38.685,
+ lng: -115.234,
+};
+
+const mapOptions = {
+ fullscreenControl: false,
+}
+
+dotenv.config({ path: __dirname + '/.env' })
+const apiKey = process.env.GOOGLE_MAPS;
+
+const script = document.createElement('script');
+script.defer = true;
+script.async = true;
+script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places,drawing`;
+document.head.appendChild(script);
+
+/**
+ * Consider integrating later: allows for drawing, circling, making shapes on map
+ */
+// const drawingManager = new window.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,
+// ],
+// },
+// });
+
+
+// options for searchbox in Google Maps Places Autocomplete API
+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 } = {};
+ private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
+ @observable private _overlayAnnoInfo: Opt<Doc>;
+ showInfo = action((anno: Opt<Doc>) => this._overlayAnnoInfo = anno);
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(MapBox, fieldKey); }
+ public get SidebarKey() { return this.fieldKey + "-sidebar"; }
+ private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void);
+ @computed get inlineTextAnnotations() { return this.allMapMarkers.filter(a => a.textInlineAnnotations); }
+
+ @observable private _map: google.maps.Map = null as unknown as google.maps.Map;
+ @observable private selectedPlace: Doc | undefined;
+ @observable private markerMap: { [id: string]: google.maps.Marker } = {};
+ // @observable private markerIdToMapMarker: { [id: string]: Doc | MapMarker | undefined } = {};
+ @observable private center = navigator.geolocation ? navigator.geolocation.getCurrentPosition : defaultCenter;
+ @observable private zoom = 2.5;
+ @observable private _marqueeing: number[] | undefined;
+ @observable private _isAnnotating = 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 _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
+ @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); };
+ @computed get allMapMarkers() { return DocListCast(this.dataDoc[this.annotationKey]); };
+ @observable private toggleAddMarker = false;
+ private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
+
+
+ @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 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.allMapMarkers.map(place => {
+ this.bounds.extend({ lat: NumCast(place.lat), lng: NumCast(place.lng) });
+ });
+ map.fitBounds(this.bounds)
+ }
+
+ /**
+ * Custom control for add marker button
+ * @param controlDiv
+ * @param map
+ */
+ private CenterControl = (controlDiv: Element) => {
+ // Set CSS for the control border.
+ const controlUI = document.createElement("div");
+
+ controlUI.style.backgroundColor = "#fff";
+ controlUI.style.border = "2px solid #fff";
+ controlUI.style.borderRadius = "3px";
+ controlUI.style.cursor = "pointer";
+ controlUI.style.marginTop = "8px";
+ controlUI.style.marginBottom = "22px";
+ controlUI.style.textAlign = "center";
+ controlUI.title = "Click to add marker to the location your pointer is at";
+ controlDiv.appendChild(controlUI);
+
+ // Set CSS for the control interior.
+ const controlText = document.createElement("div");
+
+ controlText.style.color = "rgb(25,25,25)";
+ controlText.style.fontFamily = "Roboto,Arial,sans-serif";
+ controlText.style.fontSize = "16px";
+ controlText.style.lineHeight = "38px";
+ controlText.style.paddingLeft = "5px";
+ controlText.style.paddingRight = "5px";
+ controlText.innerHTML = "Add Marker";
+ controlUI.appendChild(controlText);
+
+ // Setup the click event listeners
+ controlUI.addEventListener("click", () => {
+ if (this.toggleAddMarker == true) {
+ this.toggleAddMarker = false;
+ console.log("add marker button status:" + this.toggleAddMarker);
+ controlUI.style.backgroundColor = "#fff";
+ controlText.style.color = "rgb(25,25,25)";
+ } else {
+ this.toggleAddMarker = true;
+ console.log("add marker button status:" + this.toggleAddMarker);
+ controlUI.style.backgroundColor = "#4476f7";
+ controlText.style.color = "rgb(255,255,255)";
+ };
+ });
+ }
+
+ /**
+ * Place the marker on google maps & store the empty marker as a MapMarker Document in allMarkers list
+ * @param position - the LatLng position where the marker is placed
+ * @param map
+ */
+ @action
+ private placeMarker = (position: google.maps.LatLng, map: google.maps.Map) => {
+ const marker = new google.maps.Marker({
+ position: position,
+ map: map
+ });
+ map.panTo(position);
+ const mapMarker = Docs.Create.MapMarkerDocument(NumCast(position.lat()), NumCast(position.lng()), false, [], {});
+ this.addDocument(mapMarker, this.annotationKey);
+ }
+
+
+ /**
+ * 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;
+ const centerControlDiv = document.createElement("div");
+ this.CenterControl(centerControlDiv);
+ map.controls[google.maps.ControlPosition.TOP_RIGHT].push(centerControlDiv);
+ //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.")
+ };
+ console.log("all sidebar docs during map loading is:")
+ console.log(this.allSidebarDocs);
+ this.fitBounds(map);
+
+
+ // listener to addmarker event
+ this._map.addListener('click', (e) => {
+ console.log("add marker map status:" + this.toggleAddMarker);
+ if (this.toggleAddMarker == true) {
+ this.placeMarker(e.latLng, map)
+ console.log(this.allMapMarkers)
+ }
+ })
+ }
+
+ /**
+ * Load and render all map markers
+ * @param marker
+ * @param place
+ */
+ @action
+ private markerLoadHandler = (marker: google.maps.Marker, place: Doc) => {
+ place[Id] ? this.markerMap[place[Id]] = marker : null;
+
+ console.log("the following is a markerMap from id to Marker:")
+ console.log(this.markerMap);
+ }
+
+ /**
+ * on clicking the map marker, set the selected place to the marker document & set infowindowopen to be true
+ * @param e
+ * @param place
+ */
+ @action
+ private markerClickHandler = (e: MouseEvent, place: Doc) => {
+ // set which place was clicked
+ this.selectedPlace = place;
+
+ console.log("you have selected this location:");
+ console.log(this.selectedPlace);
+
+ place.infoWindowOpen = true;
+ console.log("open infowindow")
+ }
+
+ /**
+ * Called when dragging documents into map sidebar or directly into infowindow; to create a map marker, ref to MapMarkerDocument in Documents.ts
+ * @param doc
+ * @param sidebarKey
+ * @returns
+ */
+ sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
+ console.log("print all sidebar Docs");
+ console.log(this.allSidebarDocs);
+ if (!this.layoutDoc._showSidebar) this.toggleSidebar();
+ const docs = doc instanceof Doc ? [doc] : doc
+ docs.forEach(doc => {
+ if (doc.lat !== undefined && doc.lng !== undefined) {
+ const existingMarker = this.allMapMarkers.find(marker => marker.lat === doc.lat && marker.lng == doc.lng);
+ doc.onClickBehavior = "enterPortal";
+ if (existingMarker) {
+ Doc.AddDocToList(existingMarker, "data", doc);
+ } else {
+ const marker = Docs.Create.MapMarkerDocument(NumCast(doc.lat), NumCast(doc.lng), false, [doc], {});
+ this.addDocument(marker, this.annotationKey);
+ }
+ }
+ }) //add to annotation list
+ console.log("sidebaraddDocument");
+ console.log(doc);
+
+ return this.addDocument(doc, sidebarKey); // add to sidebar list
+ }
+
+ /**
+ * Removing documents from the sidebar
+ * @param doc
+ * @param sidebarKey
+ * @returns
+ */
+ sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
+ if (this.layoutDoc._showSidebar) this.toggleSidebar();
+ const docs = doc instanceof Doc ? [doc] : doc;
+ docs.forEach(doc => {
+ console.log(this.allMapMarkers);
+ console.log(this.allSidebarDocs);
+ })
+ return this.removeDocument(doc, sidebarKey);
+ }
+
+ /**
+ * Toggle sidebar onclick the tiny comment button on the top right corner
+ * @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 = (place: Doc) => {
+ if (place.infoWindowOpen) {
+ place.infoWindowOpen = false;
+ }
+ place.infoWindowOpen = false;
+ }
+
+ /**
+ * Handles toggle of sidebar on click the little comment button
+ */
+ @computed get sidebarHandle() {
+ TraceMobx();
+ const annotated = DocListCast(this.dataDoc[this.SidebarKey]).filter(d => d?.author).length;
+ const color = !annotated ? Colors.WHITE : Colors.BLACK;
+ const backgroundColor = !annotated ? this.sidebarWidth() ? Colors.MEDIUM_BLUE : Colors.BLACK : this.props.styleProvider?.(this.rootDoc, this.props as any, StyleProp.WidgetColor + (annotated ? ":annotated" : ""));
+ return (!annotated) ? (null) :
+ <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown}
+ style={{
+ left: `max(0px, calc(100% - ${this.sidebarWidthPercent} - 17px))`,
+ backgroundColor: backgroundColor,
+ color: color,
+ opacity: annotated ? 1 : undefined
+ }} >
+ <FontAwesomeIcon icon={"comment-alt"} />
+ </div>;
+ }
+
+ // TODO: Adding highlight box layer to Maps
+ @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;
+ }
+
+ setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => this._setPreviewCursor = func;
+
+ @action
+ onMarqueeDown = (e: React.PointerEvent) => {
+ if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) {
+ setupMoveUpEvents(this, e, action(e => {
+ MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
+ this._marqueeing = [e.clientX, e.clientY];
+ return true;
+ }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false);
+ }
+ }
+ @action finishMarquee = (x?: number, y?: number) => {
+ this._marqueeing = undefined;
+ this._isAnnotating = false;
+ x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false);
+ }
+
+ addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => {
+ return this.addDocument(doc, annotationKey);
+ }
+
+ pointerEvents = () => this.props.isContentActive() && this.props.pointerEvents !== "none" && !MarqueeOptionsMenu.Instance.isShown() ? "all" : SnappingManager.GetIsDragging() ? undefined : "none";
+
+ @computed get annotationLayer() {
+ TraceMobx();
+ const pe = this.pointerEvents();
+ return <div className="mapBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}>
+ {this.inlineTextAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno =>
+ <Annotation {...this.props} fieldKey={this.annotationKey} pointerEvents={pe} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />)}
+ </div>;
+
+ }
+
+
+ getAnchor = () => {
+ const anchor =
+ AnchorMenu.Instance?.GetAnchor(this._savedAnnotations) ??
+ this.rootDoc
+ return anchor;
+ }
+
+ infoWidth = () => this.props.PanelWidth() / 5;
+ infoHeight = () => this.props.PanelWidth() / 5;
+
+ // Collection stacking view for documents in the infowindow of a map marker
+ private renderChildDocs = (selectedDoc: Doc) => {
+ return <div style={{ width: this.infoWidth(), height: this.infoHeight() }}>
+ <CollectionStackingView {
+ ...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
+ Document={selectedDoc}
+ DataDoc={undefined}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ PanelHeight={this.infoHeight}
+ PanelWidth={this.infoWidth}
+ docFilters={returnEmptyFilter}
+ setHeight={emptyFunction}
+ isAnnotationOverlay={false}
+ select={emptyFunction}
+ scaling={returnOne}
+ isContentActive={returnTrue}
+ chromeHidden={true}
+ rootSelected={returnFalse}
+ whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
+ childHideDecorationTitle={returnTrue}
+ // childDocumentsActive={returnFalse}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={this.addDocument}
+ CollectionView={undefined}
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+ renderDepth={this.props.renderDepth + 1}
+ viewType={CollectionViewType.Stacking}
+ fieldKey={"data"}
+ pointerEvents={"all"}
+ /></div>;
+ }
+
+ /**
+ * render contents in allMapMarkers (e.g. images with exifData) into google maps as map marker
+ * @returns
+ */
+ private renderMarkers = () => {
+ return this.allMapMarkers.map(place => (
+ <Marker
+ key={place[Id]}
+ position={{ lat: NumCast(place.lat), lng: NumCast(place.lng) }}
+ onLoad={marker => this.markerLoadHandler(marker, place)}
+ onClick={e => this.markerClickHandler(e, place)}
+ />
+ ))
+ }
+
+ /**
+ * Renders infowindow corresponding to a map marker document
+ * @param place
+ * @returns
+ */
+ private renderInfoWindow = (place: Doc) => {
+
+ return place.infoWindowOpen && (
+ <InfoWindow
+ key={place[Id]}
+ anchor={this.markerMap[place[Id]]}
+ onCloseClick={() => this.handleInfoWindowClose(place)}
+ >
+ <div className="mapbox-infowindow" style={{ backgroundColor: 'white', opacity: 0.75, padding: 12, fontSize: 17 }}>
+ {this.renderChildDocs(place)}
+ <hr />
+ <div>
+ <button>New link+</button>
+ </div>
+ </div>
+ </InfoWindow>
+ )
+ }
+
+ // TODO: auto center on select a document in the sidebar
+ private handleMapCenter = (map: google.maps.Map) => {
+ console.log("print the selected views in selectionManager:")
+ if (SelectionManager.Views().lastElement()) {
+ console.log(SelectionManager.Views().lastElement());
+ }
+ }
+
+ panelWidth = () => this.props.PanelWidth() / (this.props.scaling?.() || 1) - this.sidebarWidth(); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
+ panelHeight = () => this.props.PanelHeight() / (this.props.scaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
+ scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop));
+ transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
+ opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()];
+
+ anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
+
+ render() {
+ const renderAnnotations = (docFilters?: () => string[]) =>
+ <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
+ renderDepth={this.props.renderDepth + 1}
+ isAnnotationOverlay={true}
+ fieldKey={this.annotationKey}
+ CollectionView={undefined}
+ setPreviewCursor={this.setPreviewCursor}
+ PanelWidth={this.panelWidth}
+ PanelHeight={this.panelHeight}
+ ScreenToLocalTransform={this.scrollXf}
+ scaling={returnOne}
+ dropAction={"alias"}
+ docFilters={docFilters || this.props.docFilters}
+ dontRenderDocuments={docFilters ? false : true}
+ select={emptyFunction}
+ ContentScaling={returnOne}
+ bringToFront={emptyFunction}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={this.sidebarAddDocument}
+ childPointerEvents={true}
+ pointerEvents={CurrentUserUtils.SelectedTool !== InkTool.None || this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />;
+ return <div className="mapBox" ref={this._ref}>
+ {console.log(apiKey)}
+ {/* <LoadScript
+ googleMapsApiKey={apiKey!}
+ 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})` }}>
+
+ <div style={{ mixBlendMode: "multiply" }}>
+ {renderAnnotations(this.transparentFilter)}
+ </div>
+ {renderAnnotations(this.opaqueFilter)}
+ {SnappingManager.GetIsDragging() ? (null) : renderAnnotations()}
+ {this.annotationLayer}
+ <GoogleMap
+ mapContainerStyle={mapContainerStyle}
+ zoom={this.zoom}
+ 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.renderMarkers()}
+ {this.allMapMarkers.map(place => (
+ this.renderInfoWindow(place)
+ ))}
+ {this.handleMapCenter(this._map)}
+ </GoogleMap>
+ {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) :
+ <MarqueeAnnotator rootDoc={this.rootDoc}
+ anchorMenuClick={this.anchorMenuClick}
+ scrollTop={0}
+ down={this._marqueeing} scaling={returnOne}
+ addDocument={this.addDocumentWrapper}
+ docView={this.props.docViewPath().lastElement()}
+ finishMarquee={this.finishMarquee}
+ savedAnnotations={this._savedAnnotations}
+ annotationLayer={this._annotationLayer.current}
+ mainCont={this._mainCont.current} />}
+ </div>
+ {/* </LoadScript > */}
+ <div className="mapBox-sidebar"
+ style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
+ <SidebarAnnos ref={this._sidebarRef}
+ {...this.props}
+ fieldKey={this.fieldKey}
+ rootDoc={this.rootDoc}
+ layoutDoc={this.layoutDoc}
+ dataDoc={this.dataDoc}
+ showSidebar={this.SidebarShown}
+ nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ PanelWidth={this.sidebarWidth}
+ sidebarAddDocument={this.sidebarAddDocument}
+ moveDocument={this.moveDocument}
+ removeDocument={this.sidebarRemoveDocument}
+ />
+ </div>
+ <div className="mapBox-overlayButton-sidebar" key="sidebar" title="Toggle Sidebar"
+ style={{
+ display: !this.props.isContentActive() ? "none" : undefined,
+ top: StrCast(this.rootDoc._showTitle) === "title" ? 20 : 5,
+ backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK
+ }}
+ onPointerDown={this.sidebarBtnDown} >
+ <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={"comment-alt"} size="sm" />
+ </div>
+ </div>;
+ }
+} \ No newline at end of file