diff options
author | Sophie Zhang <sophie_zhang@brown.edu> | 2024-01-25 11:35:26 -0500 |
---|---|---|
committer | Sophie Zhang <sophie_zhang@brown.edu> | 2024-01-25 11:35:26 -0500 |
commit | f3dab2a56db5e4a6a3dca58185d94e1ff7d1dc32 (patch) | |
tree | a7bc895266b53bb620dbd2dd71bad2e83b555446 /src/client/views/nodes/MapBox/MapAnchorMenu.tsx | |
parent | b5c5410b4af5d2c68d2107d3f064f6e3ec4ac3f2 (diff) | |
parent | 136f3d9f349d54e8bdd73b6380ea47c19e5edebf (diff) |
Merge branch 'master' into sophie-ai-images
Diffstat (limited to 'src/client/views/nodes/MapBox/MapAnchorMenu.tsx')
-rw-r--r-- | src/client/views/nodes/MapBox/MapAnchorMenu.tsx | 471 |
1 files changed, 431 insertions, 40 deletions
diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx index 7af4d9b59..08bea5d9d 100644 --- a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx +++ b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx @@ -1,15 +1,25 @@ -import React = require('react'); +import { IconLookup, faAdd, faArrowDown, faArrowLeft, faArrowsRotate, faBicycle, faCalendarDays, faCar, faDiamondTurnRight, faEdit, faPersonWalking, faRoute } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { IReactionDisposer, ObservableMap, reaction } from 'mobx'; +import { Autocomplete, Checkbox, FormControlLabel, TextField } from '@mui/material'; +import { IconButton } from 'browndash-components'; +import { Position } from 'geojson'; +import { IReactionDisposer, ObservableMap, action, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, Opt } from '../../../../fields/Doc'; +import * as React from 'react'; +import { CirclePicker, ColorResult } from 'react-color'; import { returnFalse, setupMoveUpEvents, unimplementedFunction } from '../../../../Utils'; +import { Doc, Opt } from '../../../../fields/Doc'; +import { NumCast, StrCast } from '../../../../fields/Types'; +import { CalendarManager } from '../../../util/CalendarManager'; import { SelectionManager } from '../../../util/SelectionManager'; -import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; -// import { GPTPopup, GPTPopupMode } from './../../GPTPopup/GPTPopup'; -import { IconButton } from 'browndash-components'; import { SettingsManager } from '../../../util/SettingsManager'; +import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; import './MapAnchorMenu.scss'; +import { MapboxApiUtility, TransportationType } from './MapboxApiUtility'; +import { MarkerIcons } from './MarkerIcons'; +// import { GPTPopup, GPTPopupMode } from './../../GPTPopup/GPTPopup'; + +type MapAnchorMenuType = 'standard' | 'routeCreation' | 'calendar' | 'customize' | 'route'; @observer export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { @@ -17,6 +27,7 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { private _disposer: IReactionDisposer | undefined; private _commentRef = React.createRef<HTMLDivElement>(); + private _fileInputRef = React.createRef<HTMLInputElement>(); public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search @@ -30,25 +41,90 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { // public MakeTargetToggle: () => void = unimplementedFunction; // public ShowTargetTrail: () => void = unimplementedFunction; public IsTargetToggler: () => boolean = returnFalse; + + public DisplayRoute: (routeInfoMap: Record<TransportationType, any> | undefined, type: TransportationType) => void = unimplementedFunction; + public AddNewRouteToMap: (coordinates: Position[], origin: string, destination: any, createPinForDestination: boolean) => void = unimplementedFunction; + public CreatePin: (feature: any) => void = unimplementedFunction; + + public UpdateMarkerColor: (color: string) => void = unimplementedFunction; + public UpdateMarkerIcon: (iconKey: string) => void = unimplementedFunction; + + public Hide: () => void = unimplementedFunction; + + public OpenAnimationPanel: (routeDoc: Doc | undefined) => void = unimplementedFunction; + + @observable + menuType: MapAnchorMenuType = 'standard'; + + @action + public setMenuType = (menuType: MapAnchorMenuType) => { + this.menuType = menuType; + }; + + private allMapPinDocs: Doc[] = []; + + private pinDoc: Doc | undefined = undefined; + + private routeDoc: Doc | undefined = undefined; + + private title: string | undefined = undefined; + + public setPinDoc(pinDoc: Doc | undefined) { + if (pinDoc) { + this.pinDoc = pinDoc; + this.title = StrCast(pinDoc.title ? pinDoc.title : `${NumCast(pinDoc.longitude)}, ${NumCast(pinDoc.latitude)}`); + } + } + + public setRouteDoc(routeDoc: Doc | undefined) { + if (routeDoc) { + this.routeDoc = routeDoc; + this.title = StrCast(routeDoc.title ?? 'Map route'); + } + } + + @action + public Reset() { + this.destinationSelected = false; + this.currentRouteInfoMap = undefined; + this.destinationFeatures = []; + this.selectedDestinationFeature = undefined; + this.allMapPinDocs = []; + this.title = undefined; + this.routeDoc = undefined; + this.pinDoc = undefined; + } + + public setAllMapboxPins(pinDocs: Doc[]) { + this.allMapPinDocs = pinDocs; + pinDocs.forEach((p, idx) => { + console.log(`Pin ${idx}: ${p.title}`); + }); + } + public get Active() { return this._left > 0; } - constructor(props: Readonly<{}>) { + constructor(props: any) { super(props); - + makeObservable(this); MapAnchorMenu.Instance = this; MapAnchorMenu.Instance._canFade = false; } componentWillUnmount() { + this.destinationFeatures = []; + this.destinationSelected = false; + this.selectedDestinationFeature = undefined; + this.currentRouteInfoMap = undefined; this._disposer?.(); } componentDidMount() { this._disposer = reaction( - () => SelectionManager.Views().slice(), - selected => MapAnchorMenu.Instance.fadeOut(true) + () => SelectionManager.Views.slice(), + sel => MapAnchorMenu.Instance.fadeOut(true) ); } // audioDown = (e: React.PointerEvent) => { @@ -81,39 +157,267 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { }; static top = React.createRef<HTMLDivElement>(); + // public get Top(){ // return this.top // } + @action + DirectionsClick = () => { + this.menuType = 'routeCreation'; + }; + + @action + CustomizeClick = () => { + this.currentRouteInfoMap = undefined; + this.menuType = 'customize'; + }; + + @action + BackClick = () => { + this.currentRouteInfoMap = undefined; + this.menuType = 'standard'; + }; + + @action + TriggerFileInputClick = () => { + if (this._fileInputRef) { + this._fileInputRef.current?.click(); // Trigger the file input click event + } + }; + + @action + onMarkerColorChange = (color: ColorResult) => { + if (this.pinDoc) { + this.pinDoc.markerColor = color.hex; + } + }; + + revertToOriginalMarker = () => { + if (this.pinDoc) { + this.pinDoc.markerType = 'MAP_PIN'; + this.pinDoc.markerColor = '#ff5722'; + } + }; + + onMarkerIconChange = (iconKey: string) => { + if (this.pinDoc) { + this.pinDoc.markerType = iconKey; + } + }; + + @observable + destinationFeatures: any[] = []; + + @observable + destinationSelected: boolean = false; + + @observable + selectedDestinationFeature: any = undefined; + + @observable + createPinForDestination: boolean = true; + + @observable + currentRouteInfoMap: Record<TransportationType, any> | undefined = undefined; + + @observable + selectedTransportationType: TransportationType = 'driving'; + + @action + handleTransportationTypeChange = (newType: TransportationType) => { + if (newType !== this.selectedTransportationType) { + this.selectedTransportationType = newType; + this.DisplayRoute(this.currentRouteInfoMap, newType); + } + }; + + @action + handleSelectedDestinationFeature = (destinationFeature: any) => { + this.selectedDestinationFeature = destinationFeature; + }; + + @action + toggleCreatePinForDestinationCheckbox = () => { + this.createPinForDestination = !this.createPinForDestination; + }; + + @action + handleDestinationSearchChange = async (searchText: string) => { + if (this.selectedDestinationFeature !== undefined) this.selectedDestinationFeature = undefined; + const features = await MapboxApiUtility.forwardGeocodeForFeatures(searchText); + if (features) { + runInAction(() => { + this.destinationFeatures = features; + }); + } + }; + + getRoutes = async (destinationFeature: any) => { + const currentPinLong: number = NumCast(this.pinDoc?.longitude); + const currentPinLat: number = NumCast(this.pinDoc?.latitude); + + if (currentPinLong && currentPinLat && destinationFeature.center) { + const routeInfoMap = await MapboxApiUtility.getDirections([currentPinLong, currentPinLat], destinationFeature.center); + if (routeInfoMap) { + runInAction(() => { + this.currentRouteInfoMap = routeInfoMap; + }); + this.DisplayRoute(routeInfoMap, 'driving'); + } + } + + // get route menu, set it equal to here + // create a temporary route + // create pin if createPinForDestination was clicked + }; + + HandleAddRouteClick = () => { + if (this.currentRouteInfoMap && this.selectedTransportationType && this.selectedDestinationFeature) { + const coordinates = this.currentRouteInfoMap[this.selectedTransportationType].coordinates; + console.log(coordinates); + console.log(this.selectedDestinationFeature); + this.AddNewRouteToMap(coordinates, this.title ?? '', this.selectedDestinationFeature, this.createPinForDestination); + } + }; + + getMarkerIcon = (): JSX.Element | undefined => { + if (this.pinDoc) { + const markerType = StrCast(this.pinDoc.markerType); + const markerColor = StrCast(this.pinDoc.markerColor); + + return MarkerIcons.getFontAwesomeIcon(markerType, '2x', markerColor); + } + return undefined; + }; + + getDirectionsButton: JSX.Element = (<IconButton tooltip="Get directions" onPointerDown={this.DirectionsClick} icon={<FontAwesomeIcon icon={faDiamondTurnRight as IconLookup} />} color={SettingsManager.userColor} />); + + getAddToCalendarButton = (docType: string): JSX.Element => { + return ( + <IconButton + tooltip="Add to calendar" + onPointerDown={() => { + CalendarManager.Instance.open(undefined, docType === 'pin' ? this.pinDoc : this.routeDoc); + }} + icon={<FontAwesomeIcon icon={faCalendarDays as IconLookup} />} + color={SettingsManager.userColor} + /> + ); + }; + addToCalendarButton: JSX.Element = ( + <IconButton tooltip="Add to calendar" onPointerDown={() => CalendarManager.Instance.open(undefined, this.pinDoc)} icon={<FontAwesomeIcon icon={faCalendarDays as IconLookup} />} color={SettingsManager.userColor} /> + ); + + getLinkNoteToDocButton = (docType: string): JSX.Element => { + return ( + <div ref={this._commentRef}> + <IconButton + tooltip={`Link Note to ${docType === 'pin' ? 'Pin' : 'Route'}`} // + onPointerDown={this.notePointerDown} + icon={<FontAwesomeIcon icon="sticky-note" />} + color={SettingsManager.userColor} + /> + </div> + ); + }; + + linkNoteToPinOrRoutenButton: JSX.Element = ( + <div ref={this._commentRef}> + <IconButton + tooltip="Link Note to Pin" // + onPointerDown={this.notePointerDown} + icon={<FontAwesomeIcon icon="sticky-note" />} + color={SettingsManager.userColor} + /> + </div> + ); + + customizePinButton: JSX.Element = (<IconButton tooltip="Customize pin" onPointerDown={this.CustomizeClick} icon={<FontAwesomeIcon icon={faEdit as IconLookup} />} color={SettingsManager.userColor} />); + + centerOnPinButton: JSX.Element = ( + <IconButton + tooltip="Center on pin" // + onPointerDown={this.Center} + icon={<FontAwesomeIcon icon="compress-arrows-alt" />} + color={SettingsManager.userColor} + /> + ); + + backButton: JSX.Element = ( + <IconButton + tooltip="Go back" // + onPointerDown={this.BackClick} + icon={<FontAwesomeIcon icon={faArrowLeft as IconLookup} />} + color={SettingsManager.userColor} + /> + ); + + addRouteButton: JSX.Element = ( + <IconButton + tooltip="Add route" // + onPointerDown={this.HandleAddRouteClick} + icon={<FontAwesomeIcon icon={faAdd as IconLookup} />} + color={SettingsManager.userColor} + /> + ); + + getDeleteButton = (type: string) => { + return ( + <IconButton + tooltip={`Delete ${type === 'pin' ? 'Pin' : 'Route'}`} // + onPointerDown={this.Delete} + icon={<FontAwesomeIcon icon="trash-alt" />} + color={SettingsManager.userColor} + /> + ); + }; + + animateRouteButton: JSX.Element = (<IconButton tooltip="Animate route" onPointerDown={() => this.OpenAnimationPanel(this.routeDoc)} icon={<FontAwesomeIcon icon={faRoute as IconLookup} />} color={SettingsManager.userColor} />); + + revertToOriginalMarkerButton = ( + <IconButton + tooltip="Revert to original" // + onPointerDown={() => this.revertToOriginalMarker()} + icon={<FontAwesomeIcon icon={faArrowsRotate as IconLookup} />} + color={SettingsManager.userColor} + /> + ); + render() { const buttons = ( - <> - { - <IconButton - tooltip="Delete Pin" // - onPointerDown={this.Delete} - icon={<FontAwesomeIcon icon="trash-alt" />} - color={SettingsManager.userColor} - /> - } - { - <div ref={this._commentRef}> - <IconButton - tooltip="Link Note to Pin" // - onPointerDown={this.notePointerDown} - icon={<FontAwesomeIcon icon="sticky-note" />} - color={SettingsManager.userColor} - /> - </div> - } - { - <IconButton - tooltip="Center on pin" // - onPointerDown={this.Center} - icon={<FontAwesomeIcon icon="compress-arrows-alt" />} - color={SettingsManager.userColor} - /> - } + <div className="menu-buttons" style={{ display: 'flex' }}> + {this.menuType === 'standard' && ( + <> + {this.getDeleteButton('pin')} + {this.getDirectionsButton} + {this.getAddToCalendarButton('pin')} + {this.getLinkNoteToDocButton('pin')} + {this.customizePinButton} + {this.centerOnPinButton} + </> + )} + {this.menuType === 'routeCreation' && ( + <> + {this.backButton} + {this.addRouteButton} + </> + )} + {this.menuType === 'route' && ( + <> + {this.getDeleteButton('route')} + {this.animateRouteButton} + {this.getAddToCalendarButton('route')} + {this.getLinkNoteToDocButton('route')} + </> + )} + {this.menuType === 'customize' && ( + <> + {this.backButton} + {this.revertToOriginalMarkerButton} + </> + )} + {/* {this.IsTargetToggler !== returnFalse && ( <Toggle tooltip={'Make target visibility toggle on click'} @@ -125,13 +429,100 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { color={SettingsManager.userColor} /> )} */} - </> + </div> ); return this.getElement( - <div ref={MapAnchorMenu.top} style={{ width: '100%', display: 'flex' }}> + <div ref={MapAnchorMenu.top} className="map-anchor-menu-container"> + {this.menuType === 'standard' && <div>{this.title}</div>} + {this.menuType === 'routeCreation' && ( + <div className="direction-inputs" style={{ display: 'flex', flexDirection: 'column' }}> + <TextField fullWidth disabled value={this.title} /> + <FontAwesomeIcon icon={faArrowDown as IconLookup} size="xs" /> + <Autocomplete + fullWidth + id="route-destination-searcher" + onInputChange={(e: any, searchText: any) => this.handleDestinationSearchChange(searchText)} + onChange={(e: any, feature: any, reason: any) => { + if (reason === 'clear') { + this.handleSelectedDestinationFeature(undefined); + } else if (reason === 'selectOption') { + this.handleSelectedDestinationFeature(feature); + } + }} + options={this.destinationFeatures.filter(feature => feature.place_name).map(feature => feature)} + getOptionLabel={(feature: any) => feature.place_name} + renderInput={(params: any) => <TextField {...params} placeholder="Enter a destination" />} + /> + {this.selectedDestinationFeature && ( + <> + {!this.allMapPinDocs.some(pinDoc => pinDoc.title === this.selectedDestinationFeature.place_name) && ( + <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '5px' }}> + <FormControlLabel label="Create pin for destination?" control={<Checkbox color="success" checked={this.createPinForDestination} onChange={this.toggleCreatePinForDestinationCheckbox} />} /> + </div> + )} + </> + )} + <button id="get-routes-button" disabled={this.selectedDestinationFeature ? false : true} onClick={() => this.getRoutes(this.selectedDestinationFeature)}> + Get routes + </button> + + {/* <input + placeholder="Origin" + /> */} + </div> + )} + {this.currentRouteInfoMap && ( + <div className="current-route-info-container"> + <div className="transportation-icons-container"> + <IconButton + tooltip="Driving route" + onPointerDown={() => this.handleTransportationTypeChange('driving')} + icon={<FontAwesomeIcon icon={faCar as IconLookup} />} + color={this.selectedTransportationType === 'driving' ? 'lightblue' : 'grey'} + /> + <IconButton + tooltip="Cycling route" + onPointerDown={() => this.handleTransportationTypeChange('cycling')} + icon={<FontAwesomeIcon icon={faBicycle as IconLookup} />} + color={this.selectedTransportationType === 'cycling' ? 'lightblue' : 'grey'} + /> + <IconButton + tooltip="Walking route" + onPointerDown={() => this.handleTransportationTypeChange('walking')} + icon={<FontAwesomeIcon icon={faPersonWalking as IconLookup} />} + color={this.selectedTransportationType === 'walking' ? 'lightblue' : 'grey'} + /> + </div> + <div className="selected-route-details-container"> + <div>Duration: {this.currentRouteInfoMap[this.selectedTransportationType].duration}</div> + <div>Distance: {this.currentRouteInfoMap[this.selectedTransportationType].distance}</div> + </div> + </div> + )} + {this.menuType === 'customize' && ( + <div className="customized-marker-container"> + <div className="current-marker-container"> + <div>Current Marker: </div> + <div>{this.getMarkerIcon()}</div> + </div> + <div className="color-picker-container" style={{ marginBottom: '10px' }}> + <CirclePicker circleSize={15} circleSpacing={7} width="100%" onChange={color => this.onMarkerColorChange(color)} /> + </div> + <div className="all-markers-container"> + {Object.keys(MarkerIcons.FAMarkerIconsMap).map(iconKey => ( + <div key={iconKey} className="marker-icon"> + <IconButton onPointerDown={() => this.onMarkerIconChange(iconKey)} icon={MarkerIcons.getFontAwesomeIcon(iconKey, '1x', 'white')} /> + </div> + ))} + </div> + <div style={{ width: '100%', height: '3px', color: 'white' }}></div> + </div> + )} + {this.menuType === 'route' && this.routeDoc && <div>{StrCast(this.routeDoc.title)}</div>} {buttons} - </div> + </div>, + true ); } } |