diff options
Diffstat (limited to 'src/client/views/nodes/MapBox')
-rw-r--r-- | src/client/views/nodes/MapBox/MapBox.scss | 40 | ||||
-rw-r--r-- | src/client/views/nodes/MapBox/MapBox.tsx | 354 | ||||
-rw-r--r-- | src/client/views/nodes/MapBox/MapMarker.tsx | 115 |
3 files changed, 509 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..4fae8d8ff --- /dev/null +++ b/src/client/views/nodes/MapBox/MapBox.scss @@ -0,0 +1,40 @@ +.mapBox { + width: 100%; + height: 100%; + overflow: hidden; + display: flex; + > div { + position: unset !important; // when the sidebar filter flys out, this prevents the map from extending outside the document box + } + + .mapBox-wrapper { + width: 100%; + .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; + } + } + + .mapBox-sidebar-handle { + position: absolute !important; + top: 0; + //top: calc(50% - 17.5px); // use this to center vertically -- make sure it looks okay for slide views + width: 10px; + height: 100%; + max-height: 35px; + background: lightgray; + border-radius: 20px; + cursor:grabbing; + } +} 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 diff --git a/src/client/views/nodes/MapBox/MapMarker.tsx b/src/client/views/nodes/MapBox/MapMarker.tsx new file mode 100644 index 000000000..34057cf48 --- /dev/null +++ b/src/client/views/nodes/MapBox/MapMarker.tsx @@ -0,0 +1,115 @@ +import { action, computed, IReactionDisposer, observable } from "mobx"; +import { observer } from "mobx-react"; +import { Transaction } from "prosemirror-state"; +import { EditorView } from "prosemirror-view"; +import * as React from "react"; +import { Doc, DocListCast, Opt } from "../../../../fields/Doc"; +import { documentSchema } from "../../../../fields/documentSchemas"; +import { Id } from "../../../../fields/FieldSymbols"; +import { createSchema, makeInterface } from "../../../../fields/Schema"; +import { Cast, NumCast } from "../../../../fields/Types"; +import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; +import { DragManager } from "../../../util/DragManager"; +import { CollectionViewType } from "../../collections/CollectionView"; +import { TabDocView } from "../../collections/TabDocView"; +import { ViewBoxAnnotatableProps, ViewBoxAnnotatableComponent } from "../../DocComponent"; +import { AnchorMenu } from "../../pdf/AnchorMenu"; +import { FieldView, FieldViewProps } from "../FieldView"; +import { FormattedTextBox } from "../formattedText/FormattedTextBox"; +import { RichTextMenu } from "../formattedText/RichTextMenu"; +import { PresMovement } from "../PresBox"; + +type MarkerDocument = makeInterface<[typeof documentSchema]>; +const MarkerDocument = makeInterface(documentSchema); + +export type Coordinates = { + lat: number, + lng: number, +} + +@observer +export class MapMarker extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps, MarkerDocument>(MarkerDocument) { + makeLinkAnchor(arg1: string, undefined: undefined, arg3: string) { + throw new Error("Method not implemented."); + } + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(MapMarker, fieldKey); } + private _markerRef: React.RefObject<google.maps.Marker> = React.createRef(); + private _disposers: { [name: string]: IReactionDisposer } = {}; + _latlngLocation!: Coordinates; + _markerId!: number; + private _editorView: Opt<EditorView> // we'll see if this becomes useful for marker annotation/create link + @observable _title: string = ""; // the title of the marker + @observable _description: string = ""; // the description of the marker contents + @observable isMarkerActive: boolean = false; // whether the marker is selected (we'll see if we need this) + @observable activeLinks: Doc[] = []; //TBD: what linking data structure looks like + @computed get childDocs() { return DocListCast(this.dataDoc[this.fieldKey]); } // a list of documents with the same/similar geographic coordinates + @computed get tagDocs() { // might come in handy for filtering + const tagDocs: Doc[] = []; + for (const doc of this.childDocs) { + const tagDoc = Cast(doc.presentationTargetDoc, Doc, null); + tagDocs.push(tagDoc); + } + return tagDocs; + } + + + /** + * Methods + */ + componentDidMount() { } + + componentWillMount() { } + + @computed private get filterAssociatedDocs() { + return + } + + addLinkToMarker = () => { } + + + + @action + setupAnchorMenu = () => { + AnchorMenu.Instance.Status = "marquee"; + AnchorMenu.Instance.Highlight = action((color: string, isLinkButton: boolean) => { + this._editorView?.state && RichTextMenu.Instance.insertHighlight(color, this._editorView.state, this._editorView?.dispatch); + return undefined; + }); + /** + * This function is used by the PDFmenu to create an anchor highlight and a new linked text annotation. + * It also initiates a Drag/Drop interaction to place the text annotation. + */ + AnchorMenu.Instance.StartDrag = action(async (e: PointerEvent, ele: HTMLElement) => { + e.preventDefault(); + e.stopPropagation(); + const targetCreator = (annotationOn?: Doc) => { + const target = CurrentUserUtils.GetNewTextDoc("Note linked to " + this.rootDoc.title, 0, 0, 100, 100, undefined, annotationOn); + FormattedTextBox.SelectOnLoad = target[Id]; + return target; + }; + + // DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docViewPath().lastElement(), this.getAnchor, targetCreator), e.pageX, e.pageY); + }); + const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to); + this.props.isSelected(true) && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom); + } + + // will see if end up using this + dispatchTransaction = (tx: Transaction) => { } + + + //will see if needed + // for inserting timestamps + insertTime = () => { } + + //for setting the title of the marker + @action + private updateTitle = () => { } + + //for updating the description of the marker + @action + private updateDescrption = () => { } + + + +}
\ No newline at end of file |