From e4eac6e4256dc320f6c767ecbad54b83459c4331 Mon Sep 17 00:00:00 2001 From: zaultavangar Date: Mon, 11 Dec 2023 14:46:58 -0500 Subject: updates to map feature --- .../views/nodes/MapBox/DirectionsAnchorMenu.tsx | 137 ++++++ src/client/views/nodes/MapBox/GeocoderControl.tsx | 107 +++++ src/client/views/nodes/MapBox/MapAnchorMenu.scss | 59 ++- src/client/views/nodes/MapBox/MapAnchorMenu.tsx | 385 ++++++++++++++- src/client/views/nodes/MapBox/MapBox.scss | 38 +- src/client/views/nodes/MapBox/MapBox.tsx | 527 ++++++++++++++++++--- src/client/views/nodes/MapBox/MapboxApiUtility.ts | 103 ++++ 7 files changed, 1256 insertions(+), 100 deletions(-) create mode 100644 src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx create mode 100644 src/client/views/nodes/MapBox/GeocoderControl.tsx create mode 100644 src/client/views/nodes/MapBox/MapboxApiUtility.ts (limited to 'src/client/views/nodes/MapBox') 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 { + static Instance: DirectionsAnchorMenu; + + private _disposer: IReactionDisposer | undefined; + + public onMakeAnchor: () => Opt = () => 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, addAsAnnotation?: boolean) => Opt = (color: string, isTargetToggler: boolean) => undefined; + public GetAnchor: (savedAnnotations: Opt>, addAsAnnotation: boolean) => Opt = (savedAnnotations: Opt>, 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(); + + // public get Top(){ + // return this.top + // } + + render() { + const buttons = ( +
+ } + color={SettingsManager.userColor} + /> + + + } + color={SettingsManager.userColor} + /> + } + color={SettingsManager.userColor} + /> +
+ ) + + return this.getElement( +
+
{this.title}
+
+ + +
+ {buttons} +
+ ) + } +} \ 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 & { +// mapboxAccessToken: string; +// marker?: Omit; +// 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( +// () => { +// 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(); +// // } 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 ( +//
+// Geocoder +//
+// ) +// } + +// 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 { @@ -17,6 +41,7 @@ export class MapAnchorMenu extends AntimodeMenu { private _disposer: IReactionDisposer | undefined; private _commentRef = React.createRef(); + private _fileInputRef = React.createRef(); public onMakeAnchor: () => Opt = () => undefined; // Method to get anchor from text search @@ -30,6 +55,32 @@ export class MapAnchorMenu extends AntimodeMenu { // public MakeTargetToggle: () => void = unimplementedFunction; // public ShowTargetTrail: () => void = unimplementedFunction; public IsTargetToggler: () => boolean = returnFalse; + + public DisplayRoute: (routeInfoMap: Record | undefined, type: TransportationType) => void = unimplementedFunction; + public HideRoute: () => void = unimplementedFunction; + public AddNewRouteToMap: (coordinates: List, 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 { } componentWillUnmount() { + this.destinationFeatures = []; + this.destinationSelected = false; + this.selectedDestinationFeature = undefined; + this.currentRouteInfoMap = undefined; this._disposer?.(); } @@ -81,39 +136,218 @@ export class MapAnchorMenu extends AntimodeMenu { }; static top = React.createRef(); + // 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 | 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 = ( - <> - { - + {this.menuType === 'standard' && + <> + } color={SettingsManager.userColor} - /> + /> + } + color={SettingsManager.userColor} + /> + } + color={SettingsManager.userColor} + /> +
+ } + color={SettingsManager.userColor} + /> +
+ } + color={SettingsManager.userColor} + /> + } + color={SettingsManager.userColor} + /> + } - { -
+ {this.menuType === 'route' && + <> } + tooltip="Go back" // + onPointerDown={this.BackClick} + icon={} color={SettingsManager.userColor} /> -
+ } + color={SettingsManager.userColor} + /> + } + color={SettingsManager.userColor} + /> + } + color={SettingsManager.userColor} + /> + } - { - } - color={SettingsManager.userColor} - /> + {this.menuType === 'customize' && + <> + } + color={SettingsManager.userColor} + /> + } + color={SettingsManager.userColor} + /> + {}} + /> + } + color={SettingsManager.userColor} + /> + } + + {/* {this.IsTargetToggler !== returnFalse && ( { color={SettingsManager.userColor} /> )} */} - + ); + // return ( + //
+ // HELLO THIS IS ANCHOR MENU + // {this.getElement(buttons)} + //
+ // ) return this.getElement( -
+
+ {this.menuType === 'standard' && +
{this.title}
+ } + {this.menuType === 'route' && +
+ + + 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) => ( + + )} + /> + {this.selectedDestinationFeature && + <> + {!this.allMapPinDocs.some(pinDoc => pinDoc.title === this.selectedDestinationFeature.place_name) && +
+ + } + /> +
+ } + + + + } + + + {/* */} +
+ } + {this.currentRouteInfoMap && +
+
+ this.handleTransportationTypeChange('driving')} + icon={} + color={this.selectedTransportationType === 'driving' ? 'lightblue': 'grey'} + /> + this.handleTransportationTypeChange('cycling')} + icon={} + color={this.selectedTransportationType === 'cycling' ? 'lightblue': 'grey'} + /> + this.handleTransportationTypeChange('walking')} + icon={} + color={this.selectedTransportationType === 'walking' ? 'lightblue': 'grey'} + /> +
+
+
Duration: {this.currentRouteInfoMap[this.selectedTransportationType].duration}
+
Distance: {this.currentRouteInfoMap[this.selectedTransportationType].distance}
+
+
+ + + } {buttons}
- ); + , 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= +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 & { + mapboxAccessToken: string, + marker?: Omit; + 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(); private _sidebarRef = React.createRef(); private _ref: React.RefObject = React.createRef(); + private _mapRef: React.RefObject = React.createRef(); private _disposers: { [key: string]: IReactionDisposer } = {}; private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt) => void); @@ -81,6 +137,26 @@ export class MapBox extends ViewBoxAnnotatableComponent 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 { - // 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 { 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 { 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 (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 { + 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 { + // 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, 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, 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 | 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 - typeof newText === 'string' && this.searchbarOnEdit(newText)} - onEnter={e => this.bingSearch()} - placeholder={this.bingSearchBarContents || 'enter city/zip/...'} - textAlign="center" + this.handleSearchChange(e.target.value)} /> - 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) => ( + + )} + /> */} + {/* 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" + /> */} + {/*
+ */} + +
+ {this.featuresFromGeocodeResults.length > 0 && ( + +

Choose a location for your pin:

+ {this.featuresFromGeocodeResults + .filter(feature => feature.place_name) + .map((feature, idx) => ( +
{ + this.handleSearchChange(""); + this.addMarkerForFeature(feature); + }} + > +
+ {feature.place_name} +
+
+ ))} +
+ )} +
+ + + + + <> + {this.allPushpins + // .filter(anno => !anno.layout_unrendered) + .map((pushpin, idx) => ( + ) => this.handleMarkerClick(e, pushpin)} + /> + ))} + + + {/* {this.mapMarkers.length > 0 && this.mapMarkers.map((marker, idx) => ( + + ))} */} - + + + {/* -
+ /> */} + {/*
{!this._mapReady ? null : this.allAnnotations @@ -793,7 +1182,7 @@ export class MapBox extends ViewBoxAnnotatableComponent ))} -
+
*/} {/* { + 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 | 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 = { + '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 -- cgit v1.2.3-70-g09d2 From bc23855777633dfd1caf7237b75c1e8fee88dff4 Mon Sep 17 00:00:00 2001 From: zaultavangar Date: Mon, 11 Dec 2023 20:37:52 -0500 Subject: new updates to map feature --- src/client/documents/Documents.ts | 25 +- src/client/views/nodes/MapBox/MapAnchorMenu.scss | 20 ++ src/client/views/nodes/MapBox/MapAnchorMenu.tsx | 76 ++++-- src/client/views/nodes/MapBox/MapBox.scss | 40 ++- src/client/views/nodes/MapBox/MapBox.tsx | 288 +++++++++++++++------ src/client/views/nodes/MapBox/MapboxApiUtility.ts | 2 + src/client/views/nodes/MapBox/MarkerIcons.tsx | 87 +++++++ .../icon_images/mapbox-marker-icon-20px-blue.png | Bin 0 -> 1623 bytes src/fields/Doc.ts | 2 +- 9 files changed, 438 insertions(+), 102 deletions(-) create mode 100644 src/client/views/nodes/MapBox/MarkerIcons.tsx create mode 100644 src/client/views/nodes/MapBox/icon_images/mapbox-marker-icon-20px-blue.png (limited to 'src/client/views/nodes/MapBox') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 02794c432..3db9f4f06 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -176,12 +176,14 @@ export class DocumentOptions { _dimUnit?: DIMt = new DimInfo("units of collectionMulti{row,col} element's width or height - 'px' or '*' for pixels or relative units"); latitude?: NUMt = new NumInfo('latitude coordinate for map views', false); longitude?: NUMt = new NumInfo('longitude coordinate for map views', false); - routeCoordinates?: LISTt = new ListInfo("stores a route's/direction's coordinates"); // for a route document, this stores the route's coordiantes + routeCoordinates?: STRt = new StrInfo("stores a route's/direction's coordinates (stringified version)"); // for a route document, this stores the route's coordiantes + markerType?: STRt = new StrInfo('Defines the marker type for a pushpin document'); + markerColor?: STRt= new StrInfo('Defines the marker color for a pushpin document'); map?: STRt = new StrInfo('text location of map'); map_type?: STRt = new StrInfo('type of map view', false); map_zoom?: NUMt = new NumInfo('zoom of a map view', false); wikiData?: STRt = new StrInfo('WikiData ID related to map location'); - description?: STRt = new StrInfo('A description of the document') + description?: STRt = new StrInfo('A description of the document'); _timecodeToShow?: NUMt = new NumInfo('the time that a document should be displayed (e.g., when an annotation shows up as a video plays)', false); _timecodeToHide?: NUMt = new NumInfo('the time that a document should be hidden', false); _width?: NUMt = new NumInfo('displayed width of a document'); @@ -783,6 +785,13 @@ export namespace Docs { options: {}, }, ], + [ + DocumentType.MAPROUTE, + { + layout: { view: CollectionView, dataField: defaultDataKey }, + options: {}, + }, + ], ]); const suffix = 'Proto'; @@ -1139,16 +1148,12 @@ export namespace Docs { documents: Array, options: DocumentOptions, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.PUSHPIN), new List(documents), { latitude, longitude, infoWindowOpen, ...options }, id); } - export function MapRouteDocument( - infoWindowOpen: boolean, - documents: Array, - options: DocumentOptions, - id?: string - ) { - return InstanceFromProto(Prototypes.get(DocumentType.MAPROUTE), new List(documents), {infoWindowOpen, ...options}, id) + export function MapRouteDocument(infoWindowOpen: boolean, documents: Array, options: DocumentOptions, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.MAPROUTE), new List(documents), { infoWindowOpen, ...options }, id); } // shouldn't ever need to create a KVP document-- instead set the LayoutTemplateString to be a KeyValueBox for the DocumentView (see addDocTab in TabDocView) @@ -2015,4 +2020,4 @@ ScriptingGlobals.add(function generateLinkTitle(link: Doc) { const link_anchor_2title = link.link_anchor_2 && link.link_anchor_2 !== link ? Cast(link.link_anchor_2, Doc, null)?.title : ''; const relation = link.link_relationship || 'to'; return `${link_anchor_1title} (${relation}) ${link_anchor_2title}`; -}); +}); \ 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 e2fcd78fc..c36d98afe 100644 --- a/src/client/views/nodes/MapBox/MapAnchorMenu.scss +++ b/src/client/views/nodes/MapBox/MapAnchorMenu.scss @@ -103,6 +103,26 @@ } + .customized-marker-container{ + display: flex; + flex-direction: column; + gap: 10px; + + .current-marker-container{ + display: flex; + align-items: center; + gap: 5px; + } + + .all-markers-container{ + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + max-width: 400px; + } + } + diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx index fca3998c8..2c2879900 100644 --- a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx +++ b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx @@ -32,6 +32,9 @@ import { Autocomplete, Checkbox, FormControlLabel, TextField } from '@mui/materi import { MapboxApiUtility, TransportationType } from './MapboxApiUtility'; import { MapBox } from './MapBox'; import { List } from '../../../../fields/List'; +import { MapboxColor, MarkerIcons } from './MarkerIcons'; +import { CirclePicker } from 'react-color'; +import { Position } from 'geojson'; type MapAnchorMenuType = 'standard' | 'route' | 'calendar' | 'customize'; @@ -58,9 +61,13 @@ export class MapAnchorMenu extends AntimodeMenu { public DisplayRoute: (routeInfoMap: Record | undefined, type: TransportationType) => void = unimplementedFunction; public HideRoute: () => void = unimplementedFunction; - public AddNewRouteToMap: (coordinates: List, origin: string, destination: string) => void = unimplementedFunction; + public AddNewRouteToMap: (coordinates: Position[], origin: string, destination: string) => void = unimplementedFunction; public CreatePin: (feature: any) => void = unimplementedFunction; + public UpdateMarkerColor: (color: string) => void = unimplementedFunction; + public UpdateMarkerIcon: (iconKey: string) => void = unimplementedFunction; + + private allMapPinDocs: Doc[] = []; @@ -151,11 +158,13 @@ export class MapAnchorMenu extends AntimodeMenu { @action CustomizeClick = () => { + this.currentRouteInfoMap = undefined; this.menuType = 'customize'; } @action BackClick = () => { + this.currentRouteInfoMap = undefined; this.menuType = 'standard'; } @@ -238,11 +247,26 @@ export class MapAnchorMenu extends AntimodeMenu { 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); + console.log(coordinates); + this.AddNewRouteToMap(coordinates, this.title ?? "", this.selectedDestinationFeature.place_name); this.HideRoute(); } } + getMarkerIcon = (): JSX.Element | undefined => { + if (this.pinDoc){ + const markerType = StrCast(this.pinDoc.markerType); + const markerColor = StrCast(this.pinDoc.markerColor); + + if (markerType.startsWith("MAPBOX")){ + return MarkerIcons.getMapboxIcon(markerColor as MapboxColor); + } else { // font awesome icon + return MarkerIcons.getFontAwesomeIcon(markerType, markerColor); + } + } + return undefined; + } + render() { const buttons = ( @@ -325,19 +349,6 @@ export class MapAnchorMenu extends AntimodeMenu { icon={} color={SettingsManager.userColor} /> - } - color={SettingsManager.userColor} - /> - {}} - /> { + } + {this.menuType === 'customize' && +
+
+
Current Marker:
+
+ {this.getMarkerIcon()} +
+
+
+ console.log(color.hex)} + /> +
+
+ {Object.keys(MarkerIcons.FAMarkerIconsMap).map((iconKey) => { + const icon = MarkerIcons.getFontAwesomeIcon(iconKey); + if (icon){ + return ( +
+ {}} + icon={MarkerIcons.getFontAwesomeIcon(iconKey, 'white')} + /> +
+ ) + } + return null; + })} +
+
+
} {buttons} diff --git a/src/client/views/nodes/MapBox/MapBox.scss b/src/client/views/nodes/MapBox/MapBox.scss index 946c6f495..bc2f90fbd 100644 --- a/src/client/views/nodes/MapBox/MapBox.scss +++ b/src/client/views/nodes/MapBox/MapBox.scss @@ -12,8 +12,10 @@ font-size: 17; } .mapBox-searchbar { - // display: flex; - // flex-direction: row; + display: flex; + flex-direction: row; + gap: 5px; + align-items: center; width: calc(100% - 40px); // .editableText-container { @@ -25,6 +27,38 @@ // } } + .mapbox-settings-panel{ + z-index: 900; + padding: 10px 20px; + display: flex; + background-color: rgb(187, 187, 187); + font-size: 1.3em; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 7px; + position: absolute; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + + .mapbox-style-select{ + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 4px; + } + + .mapbox-terrain-selection{ + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + gap: 4px; + } + + } + .mapbox-geocoding-search-results { z-index: 900; display: flex; @@ -35,6 +69,8 @@ background-color: rgb(187, 187, 187); font-size: 1.4em; padding: 10px; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; .search-result-container { width: 100%; diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index 2b563faf2..ac926e1fb 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -9,7 +9,7 @@ import * as React from 'react'; import { Doc, DocListCast, Field, LinkedTo, Opt } from '../../../../fields/Doc'; import { DocCss, Highlight } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; -import { DocCast, NumCast, StrCast } from '../../../../fields/Types'; +import { Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils'; import { Docs, DocUtils } from '../../../documents/Documents'; import { DocumentType } from '../../../documents/DocumentTypes'; @@ -52,11 +52,13 @@ import './MapBox.scss'; import { NumberLiteralType } from 'typescript'; // import { GeocoderControl } from './GeocoderControl'; import mapboxgl, { LngLat, MapLayerMouseEvent } from 'mapbox-gl'; -import { Feature, FeatureCollection } from 'geojson'; +import { Feature, FeatureCollection, GeoJsonProperties, Geometry, LineString, MultiLineString, Position } from 'geojson'; import { MarkerEvent } from 'react-map-gl/dist/esm/types'; import { MapboxApiUtility, TransportationType} from './MapboxApiUtility'; -import { Autocomplete, TextField } from '@mui/material'; +import { Autocomplete, Checkbox, FormControlLabel, TextField } from '@mui/material'; import { List } from '../../../../fields/List'; +import { listSpec } from '../../../../fields/Schema'; +import { IconLookup, faGear } from '@fortawesome/free-solid-svg-icons'; // amongus /** @@ -140,15 +142,17 @@ export class MapBox extends ViewBoxAnnotatableComponent anno.type === DocumentType.MAPROUTE); } - @computed get allRoutesGeoJson() { - const features = this.allRoutes.map(route => { + @computed get allRoutesGeoJson(): FeatureCollection { + const features: Feature[] = this.allRoutes.map(route => { + console.log("Route coords: ", route.coordinates); + const geometry: LineString = { + type: 'LineString', + coordinates: JSON.parse(StrCast(route.coordinates)) + } return { type: 'Feature', properties: {}, - geometry: { - type: 'LineString', - coordinates: route.coordinates - } + geometry: geometry }; }); @@ -771,7 +775,14 @@ export class MapBox extends ViewBoxAnnotatableComponent, origin: string, destination: string) => { + createMapRoute = undoable((coordinates: Position[], origin: string, destination: string) => { + console.log(coordinates); const mapRoute = Docs.Create.MapRouteDocument( false, [], - {title: `${origin} -> ${destination}`, routeCoordinates: coordinates}, + {title: `${origin} -> ${destination}`, routeCoordinates: JSON.stringify(coordinates)}, ); this.addDocument(mapRoute, this.annotationKey); return mapRoute; @@ -800,29 +812,11 @@ export class MapBox extends ViewBoxAnnotatableComponent { - this.mapboxMapViewState = e.viewState; - } - @action addMarkerForFeature = (feature: any) => { @@ -838,6 +832,12 @@ export class MapBox extends ViewBoxAnnotatableComponent { + this.settingsOpen= false; this.featuresFromGeocodeResults = features; }) } @@ -988,6 +989,75 @@ export class MapBox extends ViewBoxAnnotatableComponent { + this.featuresFromGeocodeResults = []; + this.settingsOpen = !this.settingsOpen; + } + + @action + changeMapStyle = (e: React.ChangeEvent) => { + this.mapStyle = `mapbox://styles/mapbox/${e.target.value}` + } + + @action + onMapMove = (e: ViewStateChangeEvent) => { + this.mapboxMapViewState = e.viewState; + } + + @action + toggleShowTerrain = () => { + this.showTerrain = !this.showTerrain; + } + + @action + onBearingChange = (e: React.ChangeEvent) => { + const newVal = parseInt(e.target.value) + if (!isNaN(newVal) && newVal >= 0){ + this.mapboxMapViewState = { + ...this.mapboxMapViewState, + bearing: parseInt(e.target.value) + } + } + } + + @action + onPitchChange = (e: React.ChangeEvent) => { + const newVal = parseInt(e.target.value); + if (!isNaN(newVal) && newVal >= 0){ + this.mapboxMapViewState = { + ...this.mapboxMapViewState, + pitch: parseInt(e.target.value) + } + } + } + + + static _firstRender = true; static _rerenderDelay = 500; @@ -1029,51 +1099,59 @@ export class MapBox extends ViewBoxAnnotatableComponent this.handleSearchChange(e.target.value)} /> - {/* 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) => ( - - )} - /> */} - {/* 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" - /> */} - {/*