aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/MapBox
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/MapBox')
-rw-r--r--src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx137
-rw-r--r--src/client/views/nodes/MapBox/GeocoderControl.tsx107
-rw-r--r--src/client/views/nodes/MapBox/MapAnchorMenu.scss59
-rw-r--r--src/client/views/nodes/MapBox/MapAnchorMenu.tsx385
-rw-r--r--src/client/views/nodes/MapBox/MapBox.scss38
-rw-r--r--src/client/views/nodes/MapBox/MapBox.tsx527
-rw-r--r--src/client/views/nodes/MapBox/MapboxApiUtility.ts103
7 files changed, 1256 insertions, 100 deletions
diff --git a/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx b/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx
new file mode 100644
index 000000000..bf4028f01
--- /dev/null
+++ b/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx
@@ -0,0 +1,137 @@
+import React = require('react');
+import { observer } from "mobx-react";
+import { AntimodeMenu, AntimodeMenuProps } from "../../AntimodeMenu";
+import { IReactionDisposer, ObservableMap, reaction } from "mobx";
+import { Doc, Opt } from "../../../../fields/Doc";
+import { returnFalse, unimplementedFunction } from "../../../../Utils";
+import { NumCast, StrCast } from "../../../../fields/Types";
+import { SelectionManager } from "../../../util/SelectionManager";
+import { IconButton } from "browndash-components";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { SettingsManager } from "../../../util/SettingsManager";
+import { IconLookup, faAdd, faCalendarDays, faRoute } from "@fortawesome/free-solid-svg-icons";
+
+@observer
+export class DirectionsAnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
+ static Instance: DirectionsAnchorMenu;
+
+ private _disposer: IReactionDisposer | undefined;
+
+ public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search
+
+ public Center: () => void = unimplementedFunction;
+ public OnClick: (e: PointerEvent) => void = unimplementedFunction;
+ // public OnAudio: (e: PointerEvent) => void = unimplementedFunction;
+ public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
+ public Highlight: (color: string, isTargetToggler: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => Opt<Doc> = (color: string, isTargetToggler: boolean) => undefined;
+ public GetAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => undefined;
+ public Delete: () => void = unimplementedFunction;
+ // public MakeTargetToggle: () => void = unimplementedFunction;
+ // public ShowTargetTrail: () => void = unimplementedFunction;
+ public IsTargetToggler: () => boolean = returnFalse;
+
+ private title: string | undefined = undefined;
+
+ public setPinDoc(pinDoc: Doc){
+ this.title = StrCast(pinDoc.title ? pinDoc.title : `${NumCast(pinDoc.longitude)}, ${NumCast(pinDoc.latitude)}`) ;
+ console.log("Title: ", this.title)
+ }
+
+ public get Active() {
+ return this._left > 0;
+ }
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+
+ DirectionsAnchorMenu.Instance = this;
+ DirectionsAnchorMenu.Instance._canFade = false;
+ }
+
+ componentWillUnmount() {
+ this._disposer?.();
+ }
+
+ componentDidMount() {
+ this._disposer = reaction(
+ () => SelectionManager.Views().slice(),
+ sel => DirectionsAnchorMenu.Instance.fadeOut(true)
+ );
+ }
+ // audioDown = (e: React.PointerEvent) => {
+ // setupMoveUpEvents(this, e, returnFalse, returnFalse, e => this.OnAudio?.(e));
+ // };
+
+ // cropDown = (e: React.PointerEvent) => {
+ // setupMoveUpEvents(
+ // this,
+ // e,
+ // (e: PointerEvent) => {
+ // this.StartCropDrag(e, this._commentCont.current!);
+ // return true;
+ // },
+ // returnFalse,
+ // e => this.OnCrop?.(e)
+ // );
+ // };
+ // notePointerDown = (e: React.PointerEvent) => {
+ // setupMoveUpEvent(
+ // this,
+ // e,
+ // (e: PointerEvent) => {
+ // this.StartDrag(e, this._commentRef.current!);
+ // return true;
+ // },
+ // returnFalse,
+ // e => this.OnClick(e)
+ // );
+ // };
+
+ static top = React.createRef<HTMLDivElement>();
+
+ // public get Top(){
+ // return this.top
+ // }
+
+ render() {
+ const buttons = (
+ <div className='directions-menu-buttons' style={{display: 'flex'}}>
+ <IconButton
+ tooltip="Add route" //
+ onPointerDown={this.Delete}
+ icon={<FontAwesomeIcon icon={faAdd as IconLookup} />}
+ color={SettingsManager.userColor}
+ />
+
+
+ <IconButton
+ tooltip='Animate route'
+ onPointerDown={this.Delete} /**TODO: fix */
+ icon={<FontAwesomeIcon icon={faRoute as IconLookup}/>}
+ color={SettingsManager.userColor}
+ />
+ <IconButton
+ tooltip='Add to calendar'
+ onPointerDown={this.Delete} /**TODO: fix */
+ icon={<FontAwesomeIcon icon={faCalendarDays as IconLookup}/>}
+ color={SettingsManager.userColor}
+ />
+ </div>
+ )
+
+ return this.getElement(
+ <div ref={DirectionsAnchorMenu.top} style={{ height: 'max-content' , width: '100%', display: 'flex', flexDirection: 'column' }}>
+ <div>{this.title}</div>
+ <div className='direction-inputs' style={{display: 'flex', flexDirection: 'column'}}>
+ <input
+ placeholder="Origin"
+ />
+ <input
+ placeholder="Destination"
+ />
+ </div>
+ {buttons}
+ </div>
+ )
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/MapBox/GeocoderControl.tsx b/src/client/views/nodes/MapBox/GeocoderControl.tsx
new file mode 100644
index 000000000..e4ba51316
--- /dev/null
+++ b/src/client/views/nodes/MapBox/GeocoderControl.tsx
@@ -0,0 +1,107 @@
+// import React from 'react';
+// import MapboxGeocoder , { GeocoderOptions} from '@mapbox/mapbox-gl-geocoder'
+// import { ControlPosition, MarkerProps, useControl } from "react-map-gl";
+
+// import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css'
+
+
+// export type GeocoderControlProps = Omit<GeocoderOptions, 'accessToken' | 'mapboxgl' | 'marker'> & {
+// mapboxAccessToken: string;
+// marker?: Omit<MarkerProps, 'longitude' | 'latitude'>;
+// position: ControlPosition;
+
+// onLoading: (...args: any[]) => void;
+// onResults: (...args: any[]) => void;
+// onResult: (...args: any[]) => void;
+// onError: (...args: any[]) => void;
+// }
+
+// export const GeocoderControl = (props: GeocoderControlProps) => {
+
+// console.log(props);
+
+// const geocoder = useControl<MapboxGeocoder>(
+// () => {
+// const ctrl = new MapboxGeocoder({
+// ...props,
+// marker: false,
+// accessToken: props.mapboxAccessToken
+// });
+// ctrl.on('loading', props.onLoading);
+// ctrl.on('results', props.onResults);
+// ctrl.on('result', evt => {
+// props.onResult(evt);
+
+// // const {result} = evt;
+// // const location =
+// // result &&
+// // (result.center || (result.geometry?.type === 'Point' && result.geometry.coordinates));
+// // if (location && props.marker) {
+// // setMarker(<Marker {...props.marker} longitude={location[0]} latitude={location[1]} />);
+// // } else {
+// // setMarker(null);
+// // }
+// });
+// ctrl.on('error', props.onError);
+// return ctrl;
+// },
+// {
+// position: props.position
+// }
+// );
+
+
+// // @ts-ignore (TS2339) private member
+// if (geocoder._map) {
+// if (geocoder.getProximity() !== props.proximity && props.proximity !== undefined) {
+// geocoder.setProximity(props.proximity);
+// }
+// if (geocoder.getRenderFunction() !== props.render && props.render !== undefined) {
+// geocoder.setRenderFunction(props.render);
+// }
+// if (geocoder.getLanguage() !== props.language && props.language !== undefined) {
+// geocoder.setLanguage(props.language);
+// }
+// if (geocoder.getZoom() !== props.zoom && props.zoom !== undefined) {
+// geocoder.setZoom(props.zoom);
+// }
+// if (geocoder.getFlyTo() !== props.flyTo && props.flyTo !== undefined) {
+// geocoder.setFlyTo(props.flyTo);
+// }
+// if (geocoder.getPlaceholder() !== props.placeholder && props.placeholder !== undefined) {
+// geocoder.setPlaceholder(props.placeholder);
+// }
+// if (geocoder.getCountries() !== props.countries && props.countries !== undefined) {
+// geocoder.setCountries(props.countries);
+// }
+// if (geocoder.getTypes() !== props.types && props.types !== undefined) {
+// geocoder.setTypes(props.types);
+// }
+// if (geocoder.getMinLength() !== props.minLength && props.minLength !== undefined) {
+// geocoder.setMinLength(props.minLength);
+// }
+// if (geocoder.getLimit() !== props.limit && props.limit !== undefined) {
+// geocoder.setLimit(props.limit);
+// }
+// if (geocoder.getFilter() !== props.filter && props.filter !== undefined) {
+// geocoder.setFilter(props.filter);
+// }
+// if (geocoder.getOrigin() !== props.origin && props.origin !== undefined) {
+// geocoder.setOrigin(props.origin);
+// }
+// }
+// return (
+// <div>
+// Geocoder
+// </div>
+// )
+// }
+
+// const noop = () => {};
+
+// GeocoderControl.defaultProps = {
+// marker: true,
+// onLoading: noop,
+// onResults: noop,
+// onError: noop
+// }; \ No newline at end of file
diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.scss b/src/client/views/nodes/MapBox/MapAnchorMenu.scss
index 6990bdcf1..e2fcd78fc 100644
--- a/src/client/views/nodes/MapBox/MapAnchorMenu.scss
+++ b/src/client/views/nodes/MapBox/MapAnchorMenu.scss
@@ -51,4 +51,61 @@
border: 2px solid white;
}
}
-} \ No newline at end of file
+}
+
+.map-anchor-menu-container {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ padding: 5px;
+ height: max-content;
+ min-width: 300px;
+
+ .direction-inputs {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+
+ #get-routes-button {
+ padding: 8px 10px;
+ border-radius: 5px;
+ }
+ }
+
+ .MuiInputBase-input{
+ color: white !important;
+ }
+
+
+ .css-1t8l2tu-MuiInputBase-input-MuiOutlinedInput-input.Mui-disabled{
+ -webkit-text-fill-color: #b3b2b2 !important;
+ }
+
+ .current-route-info-container {
+ width: 100%;
+
+ .transportation-icons-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 5px;
+ }
+
+ .selected-route-details-container{
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+ justify-content: center;
+ align-items: flex-start;
+ padding: 5px;
+ }
+
+
+ }
+
+
+
+
+}
+
+
diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx
index f6680aac0..fca3998c8 100644
--- a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx
+++ b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx
@@ -1,15 +1,39 @@
import React = require('react');
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { IReactionDisposer, ObservableMap, reaction } from 'mobx';
+import { IReactionDisposer, ObservableMap, action, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
-import { Doc, Opt } from '../../../../fields/Doc';
+import { Doc, NumListCast, Opt } from '../../../../fields/Doc';
import { returnFalse, setupMoveUpEvents, unimplementedFunction } from '../../../../Utils';
import { SelectionManager } from '../../../util/SelectionManager';
import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
// import { GPTPopup, GPTPopupMode } from './../../GPTPopup/GPTPopup';
-import { IconButton } from 'browndash-components';
+import { Button, IconButton } from 'browndash-components';
import { SettingsManager } from '../../../util/SettingsManager';
import './MapAnchorMenu.scss';
+import { NumCast, StrCast } from '../../../../fields/Types';
+import {
+ IconLookup,
+ faDiamondTurnRight,
+ faCalendarDays,
+ faEdit,
+ faAdd,
+ faRoute,
+ faArrowLeft,
+ faLocationDot,
+ faArrowDown,
+ faCar,
+ faBicycle,
+ faPersonWalking,
+ faUpload,
+ faArrowsRotate,
+ } from '@fortawesome/free-solid-svg-icons';
+import { DirectionsAnchorMenu } from './DirectionsAnchorMenu';
+import { Autocomplete, Checkbox, FormControlLabel, TextField } from '@mui/material';
+import { MapboxApiUtility, TransportationType } from './MapboxApiUtility';
+import { MapBox } from './MapBox';
+import { List } from '../../../../fields/List';
+
+type MapAnchorMenuType = 'standard' | 'route' | 'calendar' | 'customize';
@observer
export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@@ -17,6 +41,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,6 +55,32 @@ 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 HideRoute: () => void = unimplementedFunction;
+ public AddNewRouteToMap: (coordinates: List<any>, origin: string, destination: string) => void = unimplementedFunction;
+ public CreatePin: (feature: any) => void = unimplementedFunction;
+
+
+ private allMapPinDocs: Doc[] = [];
+
+ private pinDoc: Doc | undefined = undefined
+
+ private title: string | undefined = undefined;
+
+
+ public setPinDoc(pinDoc: Doc){
+ this.pinDoc = pinDoc;
+ this.title = StrCast(pinDoc.title ? pinDoc.title : `${NumCast(pinDoc.longitude)}, ${NumCast(pinDoc.latitude)}`) ;
+ }
+
+ public setAllMapboxPins(pinDocs: Doc[]) {
+ this.allMapPinDocs = pinDocs;
+ pinDocs.forEach((p, idx) => {
+ console.log(`Pin ${idx}: ${p.title}`);
+ })
+ }
+
public get Active() {
return this._left > 0;
}
@@ -42,6 +93,10 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
}
componentWillUnmount() {
+ this.destinationFeatures = [];
+ this.destinationSelected = false;
+ this.selectedDestinationFeature = undefined;
+ this.currentRouteInfoMap = undefined;
this._disposer?.();
}
@@ -81,39 +136,218 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
};
static top = React.createRef<HTMLDivElement>();
+
// public get Top(){
// return this.top
// }
+ @observable
+ menuType: MapAnchorMenuType = 'standard';
+
+ @action
+ DirectionsClick = () => {
+ this.menuType = 'route';
+ }
+
+ @action
+ CustomizeClick = () => {
+ this.menuType = 'customize';
+ }
+
+ @action
+ BackClick = () => {
+ this.menuType = 'standard';
+ }
+
+ @action
+ TriggerFileInputClick = () => {
+ if (this._fileInputRef) {
+ this._fileInputRef.current?.click(); // Trigger the file input click event
+ }
+ }
+
+
+ @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;
+ this.AddNewRouteToMap(this.currentRouteInfoMap![this.selectedTransportationType].coordinates, this.title ?? "", this.selectedDestinationFeature.place_name);
+ this.HideRoute();
+ }
+ }
+
+
render() {
const buttons = (
- <>
- {
- <IconButton
+ <div className='menu-buttons' style={{display: 'flex'}}>
+ {this.menuType === 'standard' &&
+ <>
+ <IconButton
tooltip="Delete Pin" //
onPointerDown={this.Delete}
icon={<FontAwesomeIcon icon="trash-alt" />}
color={SettingsManager.userColor}
- />
+ />
+ <IconButton
+ tooltip='Get directions'
+ onPointerDown={this.DirectionsClick} /**TODO: fix */
+ icon={<FontAwesomeIcon icon={faDiamondTurnRight as IconLookup}/>}
+ color={SettingsManager.userColor}
+ />
+ <IconButton
+ tooltip='Add to calendar'
+ onPointerDown={this.Delete} /**TODO: fix */
+ icon={<FontAwesomeIcon icon={faCalendarDays as IconLookup}/>}
+ 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="Customize pin"
+ onPointerDown={this.CustomizeClick}
+ icon={<FontAwesomeIcon icon={faEdit as IconLookup}/>}
+ color={SettingsManager.userColor}
+ />
+ <IconButton
+ tooltip="Center on pin" //
+ onPointerDown={this.Center}
+ icon={<FontAwesomeIcon icon="compress-arrows-alt" />}
+ color={SettingsManager.userColor}
+ />
+ </>
}
- {
- <div ref={this._commentRef}>
+ {this.menuType === 'route' &&
+ <>
<IconButton
- tooltip="Link Note to Pin" //
- onPointerDown={this.notePointerDown}
- icon={<FontAwesomeIcon icon="sticky-note" />}
+ tooltip="Go back" //
+ onPointerDown={this.BackClick}
+ icon={<FontAwesomeIcon icon={faArrowLeft as IconLookup} />}
color={SettingsManager.userColor}
/>
- </div>
+ <IconButton
+ tooltip="Add route" //
+ onPointerDown={this.HandleAddRouteClick}
+ icon={<FontAwesomeIcon icon={faAdd as IconLookup} />}
+ color={SettingsManager.userColor}
+ />
+ <IconButton
+ tooltip='Animate route'
+ onPointerDown={this.Delete} /**TODO: fix */
+ icon={<FontAwesomeIcon icon={faRoute as IconLookup}/>}
+ color={SettingsManager.userColor}
+ />
+ <IconButton
+ tooltip='Add to calendar'
+ onPointerDown={this.Delete} /**TODO: fix */
+ icon={<FontAwesomeIcon icon={faCalendarDays as IconLookup}/>}
+ color={SettingsManager.userColor}
+ />
+ </>
}
- {
- <IconButton
- tooltip="Center on pin" //
- onPointerDown={this.Center}
- icon={<FontAwesomeIcon icon="compress-arrows-alt" />}
- color={SettingsManager.userColor}
- />
+ {this.menuType === 'customize' &&
+ <>
+ <IconButton
+ tooltip="Go back" //
+ onPointerDown={this.BackClick}
+ icon={<FontAwesomeIcon icon={faArrowLeft as IconLookup} />}
+ color={SettingsManager.userColor}
+ />
+ <IconButton
+ tooltip="Upload image" //
+ onPointerDown={this.TriggerFileInputClick}
+ icon={<FontAwesomeIcon icon={faUpload as IconLookup} />}
+ color={SettingsManager.userColor}
+ />
+ <input
+ type="file"
+ accept="image/*" // Optionally, specify accepted file types
+ ref={this._fileInputRef}
+ style={{ display: "none" }}
+ onChange={() => {}}
+ />
+ <IconButton
+ tooltip="Revert to original" //
+ onPointerDown={this.BackClick}
+ icon={<FontAwesomeIcon icon={faArrowsRotate as IconLookup} />}
+ color={SettingsManager.userColor}
+ />
+ </>
}
+
+
{/* {this.IsTargetToggler !== returnFalse && (
<Toggle
tooltip={'Make target visibility toggle on click'}
@@ -125,13 +359,118 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
color={SettingsManager.userColor}
/>
)} */}
- </>
+ </div>
);
+ // return (
+ // <div ref={MapAnchorMenu.top} style={{zIndex: 30000, width: '100%', height: '100px'}}>
+ // HELLO THIS IS ANCHOR MENU
+ // {this.getElement(buttons)}
+ // </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 === 'route' &&
+ <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, searchText) => this.handleDestinationSearchChange(searchText)}
+ onChange={(e, feature, reason) => {
+ 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) => feature.place_name}
+ renderInput={(params) => (
+ <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>
+
+
+ }
{buttons}
</div>
- );
+ , true);
}
}
diff --git a/src/client/views/nodes/MapBox/MapBox.scss b/src/client/views/nodes/MapBox/MapBox.scss
index 242677231..946c6f495 100644
--- a/src/client/views/nodes/MapBox/MapBox.scss
+++ b/src/client/views/nodes/MapBox/MapBox.scss
@@ -12,17 +12,40 @@
font-size: 17;
}
.mapBox-searchbar {
- display: flex;
- flex-direction: row;
+ // display: flex;
+ // flex-direction: row;
width: calc(100% - 40px);
- .editableText-container {
- width: 100%;
- font-size: 16px !important;
- }
- input {
+
+ // .editableText-container {
+ // width: 100%;
+ // font-size: 16px !important;
+ // }
+ // input {
+ // width: 100%;
+ // }
+ }
+
+ .mapbox-geocoding-search-results {
+ z-index: 900;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: flex-start;
+ position: absolute;
+ background-color: rgb(187, 187, 187);
+ font-size: 1.4em;
+ padding: 10px;
+
+ .search-result-container {
width: 100%;
+ padding: 10px;
+ &:hover{
+ background-color: lighten(rgb(187, 187, 187), 10%);
+ }
}
+
}
+
.mapBox-topbar {
display: flex;
flex-direction: row;
@@ -106,3 +129,4 @@
display: block;
}
}
+
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index 9b75ca7e3..2b563faf2 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -1,7 +1,9 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import BingMapsReact from 'bingmaps-react';
+// import 'mapbox-gl/dist/mapbox-gl.css';
+
import { Button, EditableText, IconButton, Type } from 'browndash-components';
-import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, flow, toJS} from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc, DocListCast, Field, LinkedTo, Opt } from '../../../../fields/Doc';
@@ -26,7 +28,36 @@ import { FieldView, FieldViewProps } from '../FieldView';
import { FormattedTextBox } from '../formattedText/FormattedTextBox';
import { PinProps, PresBox } from '../trails';
import { MapAnchorMenu } from './MapAnchorMenu';
+import {
+ Map as MapboxMap,
+ MapRef,
+ Marker,
+ ControlPosition,
+ FullscreenControl,
+ MapProvider,
+ MarkerProps,
+ NavigationControl,
+ ScaleControl,
+ ViewState,
+ ViewStateChangeEvent,
+ useControl,
+ GeolocateControl,
+ Popup,
+ MapEvent,
+ Source,
+ Layer} from 'react-map-gl';
+import MapboxGeocoder, {GeocoderOptions} from '@mapbox/mapbox-gl-geocoder';
+import debounce from 'debounce';
import './MapBox.scss';
+import { NumberLiteralType } from 'typescript';
+// import { GeocoderControl } from './GeocoderControl';
+import mapboxgl, { LngLat, MapLayerMouseEvent } from 'mapbox-gl';
+import { Feature, FeatureCollection } from 'geojson';
+import { MarkerEvent } from 'react-map-gl/dist/esm/types';
+import { MapboxApiUtility, TransportationType} from './MapboxApiUtility';
+import { Autocomplete, TextField } from '@mui/material';
+import { List } from '../../../../fields/List';
+
// amongus
/**
* MapBox architecture:
@@ -42,6 +73,30 @@ import './MapBox.scss';
*/
const bingApiKey = process.env.BING_MAPS; // if you're running local, get a Bing Maps api key here: https://www.bingmapsportal.com/ and then add it to the .env file in the Dash-Web root directory as: _CLIENT_BING_MAPS=<your apikey>
+const MAPBOX_ACCESS_TOKEN = 'pk.eyJ1IjoiemF1bHRhdmFuZ2FyIiwiYSI6ImNscHgwNDd1MDA3MXIydm92ODdianp6cGYifQ.WFAqbhwxtMHOWSPtu0l2uQ';
+const MAPBOX_FORWARD_GEOCODE_BASE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/';
+
+const MAPBOX_REVERSE_GEOCODE_BASE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/';
+
+type PopupInfo = {
+ longitude: number,
+ latitude: number,
+ title: string,
+ description: string
+}
+
+export type GeocoderControlProps = Omit<GeocoderOptions, 'accessToken' | 'mapboxgl' | 'marker'> & {
+ mapboxAccessToken: string,
+ marker?: Omit<MarkerProps, 'longitude' | 'latitude'>;
+ position: ControlPosition;
+
+ onResult: (...args: any[]) => void;
+}
+
+type MapMarker = {
+ longitude: number,
+ latitude: number
+}
/**
* Consider integrating later: allows for drawing, circling, making shapes on map
@@ -67,6 +122,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
private _dragRef = React.createRef<HTMLDivElement>();
private _sidebarRef = React.createRef<SidebarAnnos>();
private _ref: React.RefObject<HTMLDivElement> = React.createRef();
+ private _mapRef: React.RefObject<MapRef> = React.createRef();
private _disposers: { [key: string]: IReactionDisposer } = {};
private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void);
@@ -81,6 +137,26 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@computed get allPushpins() {
return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN);
}
+ @computed get allRoutes() {
+ return this.allAnnotations.filter(anno => anno.type === DocumentType.MAPROUTE);
+ }
+ @computed get allRoutesGeoJson() {
+ const features = this.allRoutes.map(route => {
+ return {
+ type: 'Feature',
+ properties: {},
+ geometry: {
+ type: 'LineString',
+ coordinates: route.coordinates
+ }
+ };
+ });
+
+ return {
+ type: 'FeatureCollection',
+ features: features
+ };
+ }
@computed get SidebarShown() {
return this.layoutDoc._layout_showSidebar ? true : false;
}
@@ -97,8 +173,10 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
componentDidMount() {
this._unmounting = false;
this.props.setContentView?.(this);
+
}
+
_unmounting = false;
componentWillUnmount(): void {
this._unmounting = true;
@@ -327,49 +405,29 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
entityType: 'PopulatedPlace',
};
- // incrementer: number = 0;
- /*
- * Creates Pushpin doc and adds it to the list of annotations
- */
- @action
- createPushpin = undoable((latitude: number, longitude: number, map?: string) => {
- // Stores the pushpin as a MapMarkerDocument
- const pushpin = Docs.Create.PushpinDocument(
- NumCast(latitude),
- NumCast(longitude),
- false,
- [],
- { title: map ?? `lat=${latitude},lng=${longitude}`, map: map }
- // ,'pushpinIDamongus'+ this.incrementer++
- );
- this.addDocument(pushpin, this.annotationKey);
- return pushpin;
- // mapMarker.infoWindowOpen = true;
- }, 'createpin');
-
// The pin that is selected
@observable selectedPin: Doc | undefined;
@action
deselectPin = () => {
if (this.selectedPin) {
- // Removes filter
- Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPin.latitude, 'remove');
- Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'remove');
- Doc.setDocFilter(this.rootDoc, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove');
+ // // Removes filter
+ // Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPin.latitude, 'remove');
+ // Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'remove');
+ // Doc.setDocFilter(this.rootDoc, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove');
- const temp = this.selectedPin;
- if (!this._unmounting) {
- this._bingMap.current.entities.remove(this.map_docToPinMap.get(temp));
- }
- const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(temp.latitude, temp.longitude));
- this.MicrosoftMaps.Events.addHandler(newpin, 'click', (e: any) => this.pushpinClicked(temp as Doc));
- if (!this._unmounting) {
- this._bingMap.current.entities.push(newpin);
- }
- this.map_docToPinMap.set(temp, newpin);
- this.selectedPin = undefined;
- this.bingSearchBarContents = this.rootDoc.map;
+ // const temp = this.selectedPin;
+ // if (!this._unmounting) {
+ // this._bingMap.current.entities.remove(this.map_docToPinMap.get(temp));
+ // }
+ // const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(temp.latitude, temp.longitude));
+ // this.MicrosoftMaps.Events.addHandler(newpin, 'click', (e: any) => this.pushpinClicked(temp as Doc));
+ // if (!this._unmounting) {
+ // this._bingMap.current.entities.push(newpin);
+ // }
+ // this.map_docToPinMap.set(temp, newpin);
+ // this.selectedPin = undefined;
+ // this.bingSearchBarContents = this.rootDoc.map;
}
};
@@ -534,6 +592,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
tryHideMapAnchorMenu = (e: PointerEvent) => {
let target = document.elementFromPoint(e.x, e.y);
while (target) {
+ if (target.id === 'route-destination-searcher-listbox') return;
if (target === MapAnchorMenu.top.current) return;
target = target.parentElement;
}
@@ -546,11 +605,16 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@action
centerOnSelectedPin = () => {
if (this.selectedPin) {
- this.dataDoc.latitude = this.selectedPin.latitude;
- this.dataDoc.longitude = this.selectedPin.longitude;
- this.dataDoc.map = this.selectedPin.map ?? '';
- this.bingSearchBarContents = this.selectedPin.map;
+ this._mapRef.current?.flyTo({
+ center: [NumCast(this.selectedPin.longitude), NumCast(this.selectedPin.latitude)]
+ })
}
+ // if (this.selectedPin) {
+ // this.dataDoc.latitude = this.selectedPin.latitude;
+ // this.dataDoc.longitude = this.selectedPin.longitude;
+ // this.dataDoc.map = this.selectedPin.map ?? '';
+ // this.bingSearchBarContents = this.selectedPin.map;
+ // }
MapAnchorMenu.Instance.fadeOut(true);
document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu);
};
@@ -564,6 +628,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
mapTypeId: 'grayscale',
};
+
/**
* Map options
*/
@@ -582,16 +647,15 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
},
};
- @action
- searchbarOnEdit = (newText: string) => (this.bingSearchBarContents = newText);
+
recolorPin = (pin: Doc, color?: string) => {
- this._bingMap.current.entities.remove(this.map_docToPinMap.get(pin));
- this.map_docToPinMap.delete(pin);
- const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(pin.latitude, pin.longitude), color ? { color } : {});
- this.MicrosoftMaps.Events.addHandler(newpin, 'click', (e: any) => this.pushpinClicked(pin));
- this._bingMap.current.entities.push(newpin);
- this.map_docToPinMap.set(pin, newpin);
+ // this._bingMap.current.entities.remove(this.map_docToPinMap.get(pin));
+ // this.map_docToPinMap.delete(pin);
+ // const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(pin.latitude, pin.longitude), color ? { color } : {});
+ // this.MicrosoftMaps.Events.addHandler(newpin, 'click', (e: any) => this.pushpinClicked(pin));
+ // this._bingMap.current.entities.push(newpin);
+ // this.map_docToPinMap.set(pin, newpin);
};
/*
@@ -660,25 +724,25 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
setupMoveUpEvents(
e,
e,
- e => {
+ e => { // move event
if (!dragClone) {
- dragClone = this._dragRef.current?.cloneNode(true) as HTMLDivElement;
+ dragClone = this._dragRef.current?.cloneNode(true) as HTMLDivElement; // copy draggable pin
dragClone.style.position = 'absolute';
dragClone.style.zIndex = '10000';
- DragManager.Root().appendChild(dragClone);
+ DragManager.Root().appendChild(dragClone); // add clone to root
}
dragClone.style.transform = `translate(${e.clientX - 15}px, ${e.clientY - 15}px)`;
return false;
},
- e => {
+ e => { // up event
if (!dragClone) return;
DragManager.Root().removeChild(dragClone);
- let target = document.elementFromPoint(e.x, e.y);
+ let target = document.elementFromPoint(e.x, e.y); // element for specified x and y coordinates
while (target) {
- if (target === this._ref.current) {
+ if (target === this._ref.current) {
const cpt = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY);
const x = cpt[0] - (this.props.PanelWidth() - this.sidebarWidth()) / 2;
- const y = cpt[1] - 32 /* height of search bar */ - this.props.PanelHeight() / 2;
+ const y = cpt[1] - 20 /* height of search bar */ - this.props.PanelHeight() / 2;
const location = this._bingMap.current.tryPixelToLocation(new this.MicrosoftMaps.Point(x, y));
this.createPushpin(location.latitude, location.longitude);
break;
@@ -695,8 +759,236 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
);
};
+ // incrementer: number = 0;
+ /*
+ * Creates Pushpin doc and adds it to the list of annotations
+ */
+ @action
+ createPushpin = undoable((latitude: number, longitude: number, location?: string, wikiData?: string) => {
+ // Stores the pushpin as a MapMarkerDocument
+ const pushpin = Docs.Create.PushpinDocument(
+ NumCast(latitude),
+ NumCast(longitude),
+ false,
+ [],
+ {title: location ?? `lat=${latitude},lng=${longitude}`, map: location, description: "", wikiData: wikiData},
+ // { title: map ?? `lat=${latitude},lng=${longitude}`, map: map },
+ // ,'pushpinIDamongus'+ this.incrementer++
+ );
+ this.addDocument(pushpin, this.annotationKey);
+ console.log(pushpin);
+ return pushpin;
+
+ // mapMarker.infoWindowOpen = true;
+ }, 'createpin');
+
+ @action
+ createMapRoute = undoable((coordinates: List<any>, origin: string, destination: string) => {
+ const mapRoute = Docs.Create.MapRouteDocument(
+ false,
+ [],
+ {title: `${origin} -> ${destination}`, routeCoordinates: coordinates},
+ );
+ this.addDocument(mapRoute, this.annotationKey);
+ return mapRoute;
+
+ // mapMarker.infoWindowOpen = true;
+ }, 'createmaproute');
+
searchbarKeyDown = (e: any) => e.key === 'Enter' && this.bingSearch();
+
+
+
+ @observable
+ mapboxMapViewState: ViewState = {
+ zoom: 9,
+ longitude: -71.41,
+ latitude: 41.82,
+ pitch: 0,
+ bearing: 0,
+ padding: {
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0
+ }
+ }
+
+ @observable
+ featuresFromGeocodeResults: any[] = [];
+
+ @action
+ onMapMove = (e: ViewStateChangeEvent) => {
+ this.mapboxMapViewState = e.viewState;
+ }
+
+
+ @action
+ addMarkerForFeature = (feature: any) => {
+ const location = feature.place_name;
+ if (feature.center){
+ const longitude = feature.center[0];
+ const latitude = feature.center[1];
+ const wikiData = feature.properties?.wikiData;
+
+ this.createPushpin(
+ latitude,
+ longitude,
+ location,
+ wikiData
+ )
+ this.featuresFromGeocodeResults = [];
+
+ } else {
+ // TODO: handle error
+ }
+ }
+
+
+
+ /**
+ * Makes a forward geocoding API call to Mapbox to retrieve locations based on the search input
+ * @param searchText the search input (presumably a location)
+ */
+ handleSearchChange = async (searchText: string) => {
+ const features = await MapboxApiUtility.forwardGeocodeForFeatures(searchText);
+ if (features){
+ runInAction(() => {
+ this.featuresFromGeocodeResults = features;
+ })
+ }
+ // try {
+ // const url = MAPBOX_FORWARD_GEOCODE_BASE_URL + encodeURI(searchText) +'.json' +`?access_token=${MAPBOX_ACCESS_TOKEN}`;
+ // const response = await fetch(url);
+ // const data = await response.json();
+ // runInAction(() => {
+ // this.featuresFromGeocodeResults = data.features;
+ // })
+ // } catch (error: any){
+ // // TODO: handle error in better way
+ // console.log(error);
+ // }
+ }
+ // @action
+ // debouncedCall = React.useCallback(debounce(this.debouncedOnSearchBarChange, 300), []);
+
+ /**
+ * Makes a reverse geocoding API call to retrieve features corresponding to a map click (based on longitude
+ * and latitude). Sets the search results accordingly.
+ * @param e
+ */
+ handleMapClick = async (e: MapLayerMouseEvent) => {
+ e.preventDefault();
+ const lngLat: LngLat = e.lngLat;
+ const longitude: number = lngLat.lng;
+ const latitude: number = lngLat.lat;
+
+ const features = await MapboxApiUtility.reverseGeocodeForFeatures(longitude, latitude);
+ if (features){
+ runInAction(() => {
+ this.featuresFromGeocodeResults = features;
+ })
+ }
+
+ // // REVERSE GEOCODE TO GET LOCATION DETAILS
+ // try {
+ // const url = MAPBOX_REVERSE_GEOCODE_BASE_URL + encodeURI(longitude.toString() + "," + latitude.toString()) + '.json' +
+ // `?access_token=${MAPBOX_ACCESS_TOKEN}`;
+ // const response = await fetch(url);
+ // const data = await response.json();
+ // console.log("REV GEOCODE DATA: ", data);
+ // runInAction(() => {
+ // this.featuresFromGeocodeResults = data.features;
+ // })
+ // } catch (error: any){
+ // // TODO: handle error in better way
+ // console.log(error);
+ // }
+ }
+
+ @observable
+ currentPopup: PopupInfo | undefined = undefined;
+
+ @action
+ handleMarkerClick = (e: MarkerEvent<mapboxgl.Marker, MouseEvent>, pinDoc: Doc) => {
+ this.featuresFromGeocodeResults = [];
+ this.deselectPin(); // TODO: check this method
+ this.selectedPin = pinDoc;
+ // this.bingSearchBarContents = pinDoc.map;
+
+ // Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPin.latitude, 'match');
+ // Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'match');
+ Doc.setDocFilter(this.rootDoc, LinkedTo, `mapPin=${Field.toScriptString(this.selectedPin)}`, 'check');
+
+ this.recolorPin(this.selectedPin, 'green'); // TODO: check this method
+
+
+ MapAnchorMenu.Instance.Delete = this.deleteSelectedPin;
+ MapAnchorMenu.Instance.Center = this.centerOnSelectedPin;
+ MapAnchorMenu.Instance.OnClick = this.createNoteAnnotation;
+ MapAnchorMenu.Instance.StartDrag = this.startAnchorDrag;
+
+ // pass in the pinDoc
+ MapAnchorMenu.Instance.setPinDoc(pinDoc);
+ MapAnchorMenu.Instance.setAllMapboxPins(
+ this.allAnnotations.filter(anno => !anno.layout_unrendered)
+ )
+
+ MapAnchorMenu.Instance.DisplayRoute = this.displayRoute;
+ MapAnchorMenu.Instance.HideRoute = this.hideRoute;
+ MapAnchorMenu.Instance.AddNewRouteToMap = this.createMapRoute;
+ MapAnchorMenu.Instance.CreatePin = this.addMarkerForFeature;
+
+ // const longitude = NumCast(pinDoc.longitude);
+ // const latitude = NumCast(pinDoc.longitude);
+ // const x = longitude + (this.props.PanelWidth() - this.sidebarWidth()) / 2;
+ // const y = latitude + this.props.PanelHeight() / 2 + 20;
+ // const cpt = this.props.ScreenToLocalTransform().inverse().transformPoint(x, y);
+ MapAnchorMenu.Instance.jumpTo(e.originalEvent.clientX, e.originalEvent.clientY, true);
+
+ document.addEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
+ };
+
+ @observable
+ temporaryRouteSource: FeatureCollection = {
+ type: 'FeatureCollection',
+ features: []
+ }
+
+ @action
+ displayRoute = (routeInfoMap: Record<TransportationType, any> | undefined, type: TransportationType) => {
+ if (routeInfoMap){
+ const newTempRouteSource: FeatureCollection = {
+ type: 'FeatureCollection',
+ features: [
+ {
+ type: 'Feature',
+ properties: {},
+ geometry: {
+ type: 'LineString',
+ coordinates: routeInfoMap[type].coordinates
+ }
+ }
+ ]
+ }
+ // TODO: Create pin for destination
+ // TODO: Fly to point where full route will be shown
+ this.temporaryRouteSource = newTempRouteSource;
+ }
+ }
+
+ @action
+ hideRoute = () => {
+ this.temporaryRouteSource = {
+ type: 'FeatureCollection',
+ features: []
+ }
+ }
+
+
+
+
static _firstRender = true;
static _rerenderDelay = 500;
_rerenderTimeout: any;
@@ -732,14 +1024,42 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
{SnappingManager.GetIsDragging() ? null : renderAnnotations()}
<div className="mapBox-searchbar">
- <EditableText
- // editing
- setVal={(newText: string | number) => typeof newText === 'string' && this.searchbarOnEdit(newText)}
- onEnter={e => this.bingSearch()}
- placeholder={this.bingSearchBarContents || 'enter city/zip/...'}
- textAlign="center"
+ <TextField
+ fullWidth
+ placeholder='Enter a location'
+ onChange={(e) => this.handleSearchChange(e.target.value)}
/>
- <IconButton
+ {/* <Autocomplete
+ fullWidth
+ id="map-location-searcher"
+ freeSolo
+ onInputChange={(e, searchText) => this.handleSearchChange(searchText)}
+ onChange={(e, selectedOption) => {
+ this.handleSearchChange(""); // clear input
+ this.addMarkerForFeature(selectedOption);
+ }}
+ options={this.featuresFromGeocodeResults
+ .filter(feature => feature.place_name)
+ .map(feature => feature)}
+ getOptionLabel={(feature) => feature.place_name}
+ renderInput={(params) => (
+ <TextField
+ {...params}
+ placeholder='Enter a location'
+ />
+ )}
+ /> */}
+ {/* <EditableText
+ // editing
+ setVal={(newText: string | number) => typeof newText === 'string' && this.handleSearchChange(newText)}
+ // onEnter={e => this.bingSearch()}
+ onEnter={e => {}}
+ height={32}
+ // placeholder={this.bingSearchBarContents || 'Enter a location'}
+ placeholder='Enter a location'
+ textAlign="center"
+ /> */}
+ {/* <IconButton
icon={
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="magnifying-glass" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" color="#DFDFDF">
<path
@@ -752,18 +1072,87 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
/>
<div style={{ width: 30, height: 30 }} ref={this._dragRef} onPointerDown={this.dragToggle}>
<Button tooltip="drag to place a pushpin" icon={<FontAwesomeIcon size={'lg'} icon={'bullseye'} />} />
- </div>
- </div>
+ </div> */}
+ </div>
+ <div className='mapbox-geocoding-search-results'>
+ {this.featuresFromGeocodeResults.length > 0 && (
+ <React.Fragment>
+ <h4>Choose a location for your pin: </h4>
+ {this.featuresFromGeocodeResults
+ .filter(feature => feature.place_name)
+ .map((feature, idx) => (
+ <div
+ key={idx}
+ className='search-result-container'
+ onClick={() => {
+ this.handleSearchChange("");
+ this.addMarkerForFeature(feature);
+ }}
+ >
+ <div className='search-result-place-name'>
+ {feature.place_name}
+ </div>
+ </div>
+ ))}
+ </React.Fragment>
+ )}
+ </div>
+ <MapProvider>
+ <MapboxMap
+ ref={this._mapRef}
+ initialViewState={{
+ longitude: -100,
+ latitude: 40,
+ zoom: 3.5
+ }}
+ mapboxAccessToken={MAPBOX_ACCESS_TOKEN}
+ id="mapbox-map"
+ mapStyle="mapbox://styles/mapbox/streets-v9"
+ style={{height: '100%', width: '100%'}}
+ {...this.mapboxMapViewState}
+ onMove={this.onMapMove}
+ onDblClick={this.handleMapClick}
+
+
+ >
+ <Source id='temporary-route' type='geojson' data={this.temporaryRouteSource}/>
+ <Layer
+ id='temporary-route-layer'
+ type='line'
+ source='temporary-route'
+ layout={{"line-join": "round", "line-cap": "round"}}
+ paint={{"line-color": "#36454F", "line-width": 4, "line-dasharray": [1,1]}}
+ />
+ <>
+ {this.allPushpins
+ // .filter(anno => !anno.layout_unrendered)
+ .map((pushpin, idx) => (
+ <Marker
+ key={idx}
+ longitude={NumCast(pushpin.longitude)}
+ latitude={NumCast(pushpin.latitude)}
+ anchor='bottom'
+ onClick={(e: MarkerEvent<mapboxgl.Marker, MouseEvent>) => this.handleMarkerClick(e, pushpin)}
+ />
+ ))}
+ </>
+
+ {/* {this.mapMarkers.length > 0 && this.mapMarkers.map((marker, idx) => (
+ <Marker key={idx} longitude={marker.longitude} latitude={marker.latitude}/>
+ ))} */}
- <BingMapsReact
+ </MapboxMap>
+ </MapProvider>
+
+ {/* <BingMapsReact
onMapReady={this.bingMapReady} //
bingMapsKey={bingApiKey}
height="100%"
mapOptions={this.bingMapOptions}
width="100%"
viewOptions={this.bingViewOptions}
- />
- <div>
+ /> */}
+ {/* <div>
{!this._mapReady
? null
: this.allAnnotations
@@ -793,7 +1182,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
focus={returnOne}
/>
))}
- </div>
+ </div> */}
{/* <MapBoxInfoWindow
key={Docs.Create.MapMarkerDocument(NumCast(40), NumCast(40), false, [], {})[Id]}
{...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
diff --git a/src/client/views/nodes/MapBox/MapboxApiUtility.ts b/src/client/views/nodes/MapBox/MapboxApiUtility.ts
new file mode 100644
index 000000000..80962f435
--- /dev/null
+++ b/src/client/views/nodes/MapBox/MapboxApiUtility.ts
@@ -0,0 +1,103 @@
+
+const MAPBOX_FORWARD_GEOCODE_BASE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/';
+const MAPBOX_REVERSE_GEOCODE_BASE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/';
+const MAPBOX_DIRECTIONS_BASE_URL = 'https://api.mapbox.com/directions/v5/mapbox';
+const MAPBOX_ACCESS_TOKEN = 'pk.eyJ1IjoiemF1bHRhdmFuZ2FyIiwiYSI6ImNscHgwNDd1MDA3MXIydm92ODdianp6cGYifQ.WFAqbhwxtMHOWSPtu0l2uQ';
+
+export type TransportationType = 'driving' | 'cycling' | 'walking';
+
+export class MapboxApiUtility {
+
+ static forwardGeocodeForFeatures = async (searchText: string) => {
+ try {
+ const url = MAPBOX_FORWARD_GEOCODE_BASE_URL + encodeURI(searchText) +'.json' +`?access_token=${MAPBOX_ACCESS_TOKEN}`;
+ const response = await fetch(url);
+ const data = await response.json();
+ return data.features;
+ } catch (error: any){
+ // TODO: handle error in better way
+ return null;
+ }
+ }
+
+ static reverseGeocodeForFeatures = async (longitude: number, latitude: number) => {
+ try {
+ const url = MAPBOX_REVERSE_GEOCODE_BASE_URL + encodeURI(longitude.toString() + "," + latitude.toString()) + '.json' +
+ `?access_token=${MAPBOX_ACCESS_TOKEN}`;
+ const response = await fetch(url);
+ const data = await response.json();
+ return data.features;
+ } catch (error: any){
+ return null;
+ }
+ }
+
+ static getDirections = async (origin: number[], destination: number[]): Promise<Record<TransportationType, any> | undefined> => {
+ try {
+ const drivingQuery = await fetch(
+ `${MAPBOX_DIRECTIONS_BASE_URL}/driving/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}`);
+
+ const cyclingQuery = await fetch(
+ `${MAPBOX_DIRECTIONS_BASE_URL}/cycling/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}`);
+
+ const walkingQuery = await fetch(
+ `${MAPBOX_DIRECTIONS_BASE_URL}/walking/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}`);
+
+ const drivingJson = await drivingQuery.json();
+ const cyclingJson = await cyclingQuery.json();
+ const walkingJson = await walkingQuery.json();
+
+ console.log("Driving: ", drivingJson);
+ console.log("Cycling: ", cyclingJson);
+ console.log("Waling: ", walkingJson);
+
+ const routeMap = {
+ 'driving': drivingJson.routes[0],
+ 'cycling': cyclingJson.routes[0],
+ 'walking': walkingJson.routes[0]
+ }
+
+ const routeInfoMap: Record<TransportationType, any> = {
+ 'driving': {},
+ 'cycling': {},
+ 'walking': {},
+ };
+
+ Object.entries(routeMap).forEach(([key, routeData]) => {
+ const transportationTypeKey = key as TransportationType;
+ const geometry = routeData.geometry;
+ const coordinates = geometry.coordinates;
+
+ routeInfoMap[transportationTypeKey] = {
+ duration: this.secondsToMinutesHours(routeData.duration),
+ distance: this.metersToMiles(routeData.distance),
+ coordinates: coordinates
+ }
+ })
+
+ return routeInfoMap;
+
+ // return current route info, and the temporary route
+
+ } catch (error: any){
+ return undefined;
+ console.log("Error: ", error);
+ }
+ }
+
+ private static secondsToMinutesHours = (seconds: number) => {
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60).toFixed(2);
+
+ if (hours === 0){
+ return `${minutes} min`
+ } else {
+ return `${hours} hr ${minutes} min`
+ }
+ }
+
+ private static metersToMiles = (meters: number) => {
+ return `${parseFloat((meters/1609.34).toFixed(2))} mi`;
+ }
+
+} \ No newline at end of file