aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/DocumentTypes.ts1
-rw-r--r--src/client/documents/Documents.ts20
-rw-r--r--src/client/views/AntimodeMenu.scss5
-rw-r--r--src/client/views/AntimodeMenu.tsx6
-rw-r--r--src/client/views/Main.tsx1
-rw-r--r--src/client/views/MainView.tsx4
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx2
-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
-rw-r--r--src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx844
-rw-r--r--src/fields/Doc.ts2
16 files changed, 2135 insertions, 106 deletions
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 87010f770..306e9e14b 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -37,6 +37,7 @@ export enum DocumentType {
COMPARISON = 'comparison',
GROUP = 'group',
PUSHPIN = 'pushpin',
+ MAPROUTE = 'maproute',
SCRIPTDB = 'scriptdb', // database of scripts
GROUPDB = 'groupdb', // database of groups
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index dfb9f4327..02794c432 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -176,9 +176,12 @@ 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
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')
_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');
@@ -1129,10 +1132,25 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.MAP), new List(documents), options);
}
- export function PushpinDocument(latitude: number, longitude: number, infoWindowOpen: boolean, documents: Array<Doc>, options: DocumentOptions, id?: string) {
+ export function PushpinDocument(
+ latitude: number,
+ longitude: number,
+ infoWindowOpen: boolean,
+ documents: Array<Doc>,
+ 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<Doc>,
+ 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)
// export function KVPDocument(document: Doc, options: DocumentOptions = {}) {
// return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + '.kvp', ...options });
diff --git a/src/client/views/AntimodeMenu.scss b/src/client/views/AntimodeMenu.scss
index b205a0f1e..62608ec4d 100644
--- a/src/client/views/AntimodeMenu.scss
+++ b/src/client/views/AntimodeMenu.scss
@@ -15,6 +15,11 @@
align-items: center;
gap: 3px;
+ &.expanded {
+ // Conditionally unset the height when the class is applied
+ height: auto;
+ }
+
&.with-rows {
flex-direction: column
}
diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx
index 16e76694d..a6938acbd 100644
--- a/src/client/views/AntimodeMenu.tsx
+++ b/src/client/views/AntimodeMenu.tsx
@@ -139,10 +139,12 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends React.Co
return <div className="antimodeMenu-dragger" key="dragger" onPointerDown={this.dragStart} style={{ width: '20px' }} />;
};
- protected getElement(buttons: JSX.Element) {
+ protected getElement(buttons: JSX.Element, expanded: boolean = false) {
+ const containerClass = expanded ? 'antimodeMenu-cont expanded' : 'antimodeMenu-cont';
+
return (
<div
- className="antimodeMenu-cont"
+ className={containerClass}
onPointerLeave={this.pointerLeave}
onPointerEnter={this.pointerEntered}
ref={this._mainCont}
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 96bd52d39..5ad33f5fd 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -15,6 +15,7 @@ import { CollectionView } from './collections/CollectionView';
import './global/globalScripts';
import { MainView } from './MainView';
import { BranchingTrailManager } from '../util/BranchingTrailManager';
+
dotenv.config();
AssignAllExtensions();
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index ce47a46c3..13f7dc896 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -71,6 +71,7 @@ import { PreviewCursor } from './PreviewCursor';
import { PropertiesView } from './PropertiesView';
import { DashboardStyleProvider, DefaultStyleProvider } from './StyleProvider';
import { TopBar } from './topbar/TopBar';
+import { DirectionsAnchorMenu } from './nodes/MapBox/DirectionsAnchorMenu';
const _global = (window /* browser */ || global) /* node */ as any;
@observer
@@ -982,7 +983,7 @@ export class MainView extends React.Component {
@observable mapBoxHackBool = false;
@computed get mapBoxHack() {
return this.mapBoxHackBool ? null : (
- <MapBox
+ <MapBox
ref={action((r: any) => r && (this.mapBoxHackBool = true))}
fieldKey="data"
select={returnFalse}
@@ -1071,6 +1072,7 @@ export class MainView extends React.Component {
<RadialMenu />
<AnchorMenu />
<MapAnchorMenu />
+ <DirectionsAnchorMenu/>
<DashFieldViewMenu />
<MarqueeOptionsMenu />
<TimelineMenu />
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index eed787b6d..669d45ca5 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -269,7 +269,7 @@ export class DocumentContentsView extends React.Component<
PhysicsSimulationBox,
SchemaRowBox,
ImportElementBox,
- MapPushpinBox,
+ // MapPushpinBox,
}}
bindings={bindings}
jsx={layoutFrame}
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
diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
new file mode 100644
index 000000000..a6182991d
--- /dev/null
+++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
@@ -0,0 +1,844 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import BingMapsReact from 'bingmaps-react';
+import { Button, EditableText, IconButton, Type } from 'browndash-components';
+import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+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 { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { Docs, DocUtils } from '../../../documents/Documents';
+import { DocumentType } from '../../../documents/DocumentTypes';
+import { DocumentManager } from '../../../util/DocumentManager';
+import { DragManager } from '../../../util/DragManager';
+import { LinkManager } from '../../../util/LinkManager';
+import { SnappingManager } from '../../../util/SnappingManager';
+import { Transform } from '../../../util/Transform';
+import { undoable, UndoManager } from '../../../util/UndoManager';
+import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm';
+import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../../DocComponent';
+import { Colors } from '../../global/globalEnums';
+import { SidebarAnnos } from '../../SidebarAnnos';
+import { DocumentView } from '../DocumentView';
+import { FieldView, FieldViewProps } from '../FieldView';
+import { FormattedTextBox } from '../formattedText/FormattedTextBox';
+import { PinProps, PresBox } from '../trails';
+import './MapBox.scss';
+import { MapAnchorMenu } from '../MapBox/MapAnchorMenu';
+import { MapProvider, Map as MapboxMap } from 'react-map-gl';
+
+// amongus
+/**
+ * MapBox architecture:
+ * Main component: MapBox.tsx
+ * Supporting Components: SidebarAnnos, CollectionStackingView
+ *
+ * MapBox is a node that extends the ViewBoxAnnotatableComponent. Similar to PDFBox and WebBox, it supports interaction between sidebar content and document content.
+ * The main body of MapBox uses Google Maps API to allow location retrieval, adding map markers, pan and zoom, and open street view.
+ * Dash Document architecture is integrated with Maps API: When drag and dropping documents with ExifData (gps Latitude and Longitude information) available,
+ * sidebarAddDocument function checks if the document contains lat & lng information, if it does, then the document is added to both the sidebar and the infowindow (a pop up corresponding to a map marker--pin on map).
+ * The lat and lng field of the document is filled when importing (spec see ConvertDMSToDD method and processFileUpload method in Documents.ts).
+ * A map marker is considered a document that contains a collection with stacking view of documents, it has a lat, lng location, which is passed to Maps API's custom marker (red pin) to be rendered on the google maps
+ */
+
+const mapboxApiKey = "pk.eyJ1IjoiemF1bHRhdmFuZ2FyIiwiYSI6ImNsbnc2eHJpbTA1ZTUyam85aGx4Z2FhbGwifQ.2Kqw9mk-9wAAg9kmHmKzcg";
+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>
+
+/**
+ * Consider integrating later: allows for drawing, circling, making shapes on map
+ */
+// const drawingManager = new window.google.maps.drawing.DrawingManager({
+// drawingControl: true,
+// drawingControlOptions: {
+// position: google.maps.ControlPosition.TOP_RIGHT,
+// drawingModes: [
+// google.maps.drawing.OverlayType.MARKER,
+// // currently we are not supporting the following drawing mode on map, a thought for future development
+// google.maps.drawing.OverlayType.CIRCLE,
+// google.maps.drawing.OverlayType.POLYLINE,
+// ],
+// },
+// });
+
+@observer
+export class MapBoxContainer extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(MapBoxContainer, fieldKey);
+ }
+ private _dragRef = React.createRef<HTMLDivElement>();
+ private _sidebarRef = React.createRef<SidebarAnnos>();
+ private _ref: React.RefObject<HTMLDivElement> = React.createRef();
+ private _disposers: { [key: string]: IReactionDisposer } = {};
+ private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void);
+
+ @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
+ @computed get allSidebarDocs() {
+ return DocListCast(this.dataDoc[this.SidebarKey]);
+ }
+ // this list contains pushpins and configs
+ @computed get allAnnotations() {
+ return DocListCast(this.dataDoc[this.annotationKey]);
+ }
+ @computed get allPushpins() {
+ return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN);
+ }
+ @computed get SidebarShown() {
+ return this.layoutDoc._layout_showSidebar ? true : false;
+ }
+ @computed get sidebarWidthPercent() {
+ return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%');
+ }
+ @computed get sidebarColor() {
+ return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4'));
+ }
+ @computed get SidebarKey() {
+ return this.fieldKey + '_sidebar';
+ }
+
+ componentDidMount() {
+ this._unmounting = false;
+ this.props.setContentView?.(this);
+ }
+
+ _unmounting = false;
+ componentWillUnmount(): void {
+ this._unmounting = true;
+ this.deselectPin();
+ this._rerenderTimeout && clearTimeout(this._rerenderTimeout);
+ Object.keys(this._disposers).forEach(key => this._disposers[key]?.());
+ }
+
+ /**
+ * Called when dragging documents into map sidebar or directly into infowindow; to create a map marker, ref to MapMarkerDocument in Documents.ts
+ * @param doc
+ * @param sidebarKey
+ * @returns
+ */
+ sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
+ if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar();
+ const docs = doc instanceof Doc ? [doc] : doc;
+ docs.forEach(doc => {
+ let existingPin = this.allPushpins.find(pin => pin.latitude === doc.latitude && pin.longitude === doc.longitude) ?? this.selectedPin;
+ if (doc.latitude !== undefined && doc.longitude !== undefined && !existingPin) {
+ existingPin = this.createPushpin(NumCast(doc.latitude), NumCast(doc.longitude), StrCast(doc.map));
+ }
+ if (existingPin) {
+ setTimeout(() => {
+ // we use a timeout in case this is called from the sidebar which may have just added a link that hasn't made its way into th elink manager yet
+ if (!LinkManager.Instance.getAllRelatedLinks(doc).some(link => DocCast(link.link_anchor_1)?.mapPin === existingPin || DocCast(link.link_anchor_2)?.mapPin === existingPin)) {
+ const anchor = this.getAnchor(true, undefined, existingPin);
+ anchor && DocUtils.MakeLink(anchor, doc, { link_relationship: 'link to map location' });
+ doc.latitude = existingPin?.latitude;
+ doc.longitude = existingPin?.longitude;
+ }
+ });
+ }
+ }); //add to annotation list
+
+ return this.addDocument(doc, sidebarKey); // add to sidebar list
+ };
+
+ removeMapDocument = (doc: Doc | Doc[], annotationKey?: string) => {
+ const docs = doc instanceof Doc ? [doc] : doc;
+ this.allAnnotations.filter(anno => docs.includes(DocCast(anno.mapPin))).forEach(anno => (anno.mapPin = undefined));
+ return this.removeDocument(doc, annotationKey, undefined);
+ };
+
+ /**
+ * Removing documents from the sidebar
+ * @param doc
+ * @param sidebarKey
+ * @returns
+ */
+ sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => this.removeMapDocument(doc, sidebarKey);
+
+ /**
+ * Toggle sidebar onclick the tiny comment button on the top right corner
+ * @param e
+ */
+ sidebarBtnDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(
+ this,
+ e,
+ (e, down, delta) =>
+ runInAction(() => {
+ const localDelta = this.props
+ .ScreenToLocalTransform()
+ .scale(this.props.NativeDimScaling?.() || 1)
+ .transformDirection(delta[0], delta[1]);
+ const fullWidth = NumCast(this.layoutDoc._width);
+ const mapWidth = fullWidth - this.sidebarWidth();
+ if (this.sidebarWidth() + localDelta[0] > 0) {
+ this.layoutDoc._layout_showSidebar = true;
+ this.layoutDoc._width = fullWidth + localDelta[0];
+ this.layoutDoc._layout_sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth + localDelta[0])).toString() + '%';
+ } else {
+ this.layoutDoc._layout_showSidebar = false;
+ this.layoutDoc._width = mapWidth;
+ this.layoutDoc._layout_sidebarWidthPercent = '0%';
+ }
+ return false;
+ }),
+ emptyFunction,
+ () => UndoManager.RunInBatch(this.toggleSidebar, 'toggle sidebar map')
+ );
+ };
+ sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth();
+
+ /**
+ * Handles toggle of sidebar on click the little comment button
+ */
+ @computed get sidebarHandle() {
+ return (
+ <div
+ className="mapBox-overlayButton-sidebar"
+ key="sidebar"
+ title="Toggle Sidebar"
+ style={{
+ display: !this.props.isContentActive() ? 'none' : undefined,
+ top: StrCast(this.rootDoc._layout_showTitle) === 'title' ? 20 : 5,
+ backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
+ }}
+ onPointerDown={this.sidebarBtnDown}>
+ <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" />
+ </div>
+ );
+ }
+
+ // TODO: Adding highlight box layer to Maps
+ @action
+ toggleSidebar = () => {
+ const prevWidth = this.sidebarWidth();
+ this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(100 * 0.2) / 1.2}%` : '0%') !== '0%';
+ this.layoutDoc._width = this.layoutDoc._layout_showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth);
+ };
+
+ startAnchorDrag = (e: PointerEvent, ele: HTMLElement) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const sourceAnchorCreator = action(() => {
+ const note = this.getAnchor(true);
+ if (note && this.selectedPin) {
+ note.latitude = this.selectedPin.latitude;
+ note.longitude = this.selectedPin.longitude;
+ note.map = this.selectedPin.map;
+ }
+ return note as Doc;
+ });
+
+ const targetCreator = (annotationOn: Doc | undefined) => {
+ const target = DocUtils.GetNewTextDoc('Note linked to ' + this.rootDoc.title, 0, 0, 100, 100, undefined, annotationOn, undefined, 'yellow');
+ FormattedTextBox.SelectOnLoad = target[Id];
+ return target;
+ };
+ const docView = this.props.DocumentView?.();
+ docView &&
+ DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
+ dragComplete: e => {
+ if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) {
+ e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.Document;
+ e.annoDragData.linkSourceDoc.followLinkZoom = false;
+ }
+ },
+ });
+ };
+
+ createNoteAnnotation = () => {
+ const createFunc = undoable(
+ action(() => {
+ const note = this._sidebarRef.current?.anchorMenuClick(this.getAnchor(true), ['latitude', 'longitude', LinkedTo]);
+ if (note && this.selectedPin) {
+ note.latitude = this.selectedPin.latitude;
+ note.longitude = this.selectedPin.longitude;
+ note.map = this.selectedPin.map;
+ }
+ }),
+ 'create note annotation'
+ );
+ if (!this.layoutDoc.layout_showSidebar) {
+ this.toggleSidebar();
+ setTimeout(createFunc);
+ } else createFunc();
+ };
+ sidebarDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), true);
+ };
+ sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => {
+ const bounds = this._ref.current!.getBoundingClientRect();
+ this.layoutDoc._layout_sidebarWidthPercent = '' + 100 * Math.max(0, 1 - (e.clientX - bounds.left) / bounds.width) + '%';
+ this.layoutDoc._layout_showSidebar = this.layoutDoc._layout_sidebarWidthPercent !== '0%';
+ e.preventDefault();
+ return false;
+ };
+
+ setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => (this._setPreviewCursor = func);
+
+ addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => this.addDocument(doc, annotationKey);
+
+ pointerEvents = () => (this.props.isContentActive() && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : 'none');
+
+ panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth();
+ panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
+ scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop));
+ transparentFilter = () => [...this.props.childFilters(), Utils.TransparentBackgroundFilter];
+ opaqueFilter = () => [...this.props.childFilters(), Utils.OpaqueBackgroundFilter];
+ infoWidth = () => this.props.PanelWidth() / 5;
+ infoHeight = () => this.props.PanelHeight() / 5;
+ anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
+ savedAnnotations = () => this._savedAnnotations;
+
+ _bingSearchManager: any;
+ _bingMap: any;
+ get MicrosoftMaps() {
+ return (window as any).Microsoft.Maps;
+ }
+ // uses Bing Search to retrieve lat/lng for a location. eg.,
+ // const results = this.geocodeQuery(map.map, 'Philadelphia, PA');
+ // to move the map to that location:
+ // const location = await this.geocodeQuery(this._bingMap, 'Philadelphia, PA');
+ // this._bingMap.current.setView({
+ // mapTypeId: this.MicrosoftMaps.MapTypeId.aerial,
+ // center: new this.MicrosoftMaps.Location(loc.latitude, loc.longitude),
+ // });
+ //
+ bingGeocode = (map: any, query: string) => {
+ return new Promise<{ latitude: number; longitude: number }>((res, reject) => {
+ //If search manager is not defined, load the search module.
+ if (!this._bingSearchManager) {
+ //Create an instance of the search manager and call the geocodeQuery function again.
+ this.MicrosoftMaps.loadModule('Microsoft.Maps.Search', () => {
+ this._bingSearchManager = new this.MicrosoftMaps.Search.SearchManager(map.current);
+ res(this.bingGeocode(map, query));
+ });
+ } else {
+ this._bingSearchManager.geocode({
+ where: query,
+ callback: action((r: any) => res(r.results[0].location)),
+ errorCallback: (e: any) => reject(),
+ });
+ }
+ });
+ };
+
+ @observable
+ bingSearchBarContents: any = this.rootDoc.map; // For Bing Maps: The contents of the Bing search bar (string)
+
+ geoDataRequestOptions = {
+ 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');
+
+ 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;
+ }
+ };
+
+ getView = async (doc: Doc) => {
+ if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar();
+ return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
+ };
+ /*
+ * Pushpin onclick
+ */
+ @action
+ pushpinClicked = (pinDoc: Doc) => {
+ this.deselectPin();
+ 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');
+
+ MapAnchorMenu.Instance.Delete = this.deleteSelectedPin;
+ MapAnchorMenu.Instance.Center = this.centerOnSelectedPin;
+ MapAnchorMenu.Instance.OnClick = this.createNoteAnnotation;
+ MapAnchorMenu.Instance.StartDrag = this.startAnchorDrag;
+
+ const point = this._bingMap.current.tryLocationToPixel(new this.MicrosoftMaps.Location(this.selectedPin.latitude, this.selectedPin.longitude));
+ const x = point.x + (this.props.PanelWidth() - this.sidebarWidth()) / 2;
+ const y = point.y + this.props.PanelHeight() / 2 + 32;
+ const cpt = this.props.ScreenToLocalTransform().inverse().transformPoint(x, y);
+ MapAnchorMenu.Instance.jumpTo(cpt[0], cpt[1], true);
+
+ document.addEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
+ };
+
+ /**
+ * Map OnClick
+ */
+ @action
+ mapOnClick = (e: { location: { latitude: any; longitude: any } }) => {
+ this.props.select(false);
+ this.deselectPin();
+ };
+ /*
+ * Updates values of layout doc to match the current map
+ */
+ @action
+ mapRecentered = () => {
+ if (
+ Math.abs(NumCast(this.dataDoc.latitude) - this._bingMap.current.getCenter().latitude) > 1e-7 || //
+ Math.abs(NumCast(this.dataDoc.longitude) - this._bingMap.current.getCenter().longitude) > 1e-7
+ ) {
+ this.dataDoc.latitude = this._bingMap.current.getCenter().latitude;
+ this.dataDoc.longitude = this._bingMap.current.getCenter().longitude;
+ this.dataDoc.map = '';
+ this.bingSearchBarContents = '';
+ }
+ this.dataDoc.map_zoom = this._bingMap.current.getZoom();
+ };
+ /*
+ * Updates maptype
+ */
+ @action
+ updateMapType = () => (this.dataDoc.map_type = this._bingMap.current.getMapTypeId());
+
+ /*
+ * For Bing Maps
+ * Called by search button's onClick
+ * Finds the geocode of the searched contents and sets location to that location
+ **/
+ @action
+ bingSearch = () => {
+ return this.bingGeocode(this._bingMap, this.bingSearchBarContents).then(location => {
+ this.dataDoc.latitude = location.latitude;
+ this.dataDoc.longitude = location.longitude;
+ this.dataDoc.map_zoom = this._bingMap.current.getZoom();
+ this.dataDoc.map = this.bingSearchBarContents;
+ });
+ };
+
+ /*
+ * Returns doc w/ relevant info
+ */
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps, existingPin?: Doc) => {
+ /// this should use SELECTED pushpin for lat/long if there is a selection, otherwise CENTER
+ const anchor = Docs.Create.ConfigDocument({
+ title: 'MapAnchor:' + this.rootDoc.title,
+ text: StrCast(this.selectedPin?.map) || StrCast(this.rootDoc.map) || 'map location',
+ config_latitude: NumCast((existingPin ?? this.selectedPin)?.latitude ?? this.dataDoc.latitude),
+ config_longitude: NumCast((existingPin ?? this.selectedPin)?.longitude ?? this.dataDoc.longitude),
+ config_map_zoom: NumCast(this.dataDoc.map_zoom),
+ config_map_type: StrCast(this.dataDoc.map_type),
+ config_map: StrCast((existingPin ?? this.selectedPin)?.map) || StrCast(this.dataDoc.map),
+ layout_unrendered: true,
+ mapPin: existingPin ?? this.selectedPin,
+ annotationOn: this.rootDoc,
+ });
+ if (anchor) {
+ if (!addAsAnnotation) anchor.backgroundColor = 'transparent';
+ addAsAnnotation && this.addDocument(anchor);
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), map: true } }, this.rootDoc);
+ return anchor;
+ }
+ return this.rootDoc;
+ };
+
+ map_docToPinMap = new Map<Doc, any>();
+ map_pinHighlighted = new Map<Doc, boolean>();
+ /*
+ * Input: pin doc
+ * Adds MicrosoftMaps Pushpin to the map (render)
+ */
+ @action
+ addPushpin = (pin: Doc) => {
+ const pushPin = pin.infoWindowOpen
+ ? new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(pin.latitude, pin.longitude), {})
+ : new this.MicrosoftMaps.Pushpin(
+ new this.MicrosoftMaps.Location(pin.latitude, pin.longitude)
+ // {icon: 'http://icons.iconarchive.com/icons/icons-land/vista-map-markers/24/Map-Marker-Marker-Outside-Chartreuse-icon.png'}
+ );
+
+ this._bingMap.current.entities.push(pushPin);
+
+ this.MicrosoftMaps.Events.addHandler(pushPin, 'click', (e: any) => this.pushpinClicked(pin));
+ // this.MicrosoftMaps.Events.addHandler(pushPin, 'dblclick', (e: any) => this.pushpinDblClicked(pushPin, pin));
+ this.map_docToPinMap.set(pin, pushPin);
+ };
+
+ /*
+ * Input: pin doc
+ * Removes pin from annotations
+ */
+ @action
+ removePushpin = (pinDoc: Doc) => this.removeMapDocument(pinDoc, this.annotationKey);
+
+ /*
+ * Removes pushpin from map render
+ */
+ deletePushpin = (pinDoc: Doc) => {
+ if (!this._unmounting) {
+ this._bingMap.current.entities.remove(this.map_docToPinMap.get(pinDoc));
+ }
+ this.map_docToPinMap.delete(pinDoc);
+ this.selectedPin = undefined;
+ };
+
+ @action
+ deleteSelectedPin = undoable(() => {
+ 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');
+
+ this.removePushpin(this.selectedPin);
+ }
+ MapAnchorMenu.Instance.fadeOut(true);
+ document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
+ }, 'delete pin');
+
+ tryHideMapAnchorMenu = (e: PointerEvent) => {
+ let target = document.elementFromPoint(e.x, e.y);
+ while (target) {
+ if (target === MapAnchorMenu.top.current) return;
+ target = target.parentElement;
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ MapAnchorMenu.Instance.fadeOut(true);
+ document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
+ };
+
+ @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;
+ }
+ MapAnchorMenu.Instance.fadeOut(true);
+ document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu);
+ };
+
+ /**
+ * View options for bing maps
+ */
+ bingViewOptions = {
+ // center: { latitude: this.dataDoc.latitude ?? defaultCenter.lat, longitude: this.dataDoc.longitude ?? defaultCenter.lng },
+ zoom: this.dataDoc.latitude ?? 10,
+ mapTypeId: 'grayscale',
+ };
+
+ /**
+ * Map options
+ */
+ bingMapOptions = {
+ navigationBarMode: 'square',
+ backgroundColor: '#f1f3f4',
+ enableInertia: true,
+ supportedMapTypes: ['grayscale', 'canvasLight'],
+ disableMapTypeSelectorMouseOver: true,
+ // showScalebar:true
+ // disableRoadView:true,
+ // disableBirdseye:true
+ streetsideOptions: {
+ showProblemReporting: false,
+ showCurrentAddress: false,
+ },
+ };
+
+ @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);
+ };
+
+ /*
+ * Called when BingMap is first rendered
+ * Initializes starting values
+ */
+ @observable _mapReady = false;
+ @action
+ bingMapReady = (map: any) => {
+ this._mapReady = true;
+ this._bingMap = map.map;
+ if (!this._bingMap.current) {
+ alert('NO Map!?');
+ }
+ this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'click', this.mapOnClick);
+ this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'viewchangeend', undoable(this.mapRecentered, 'Map Layout Change'));
+ this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'maptypechanged', undoable(this.updateMapType, 'Map ViewType Change'));
+
+ this._disposers.mapLocation = reaction(
+ () => this.rootDoc.map,
+ mapLoc => (this.bingSearchBarContents = mapLoc),
+ { fireImmediately: true }
+ );
+ this._disposers.highlight = reaction(
+ () => this.allAnnotations.map(doc => doc[Highlight]),
+ () => {
+ const allConfigPins = this.allAnnotations.map(doc => ({ doc, pushpin: DocCast(doc.mapPin) })).filter(pair => pair.pushpin);
+ allConfigPins.forEach(({ doc, pushpin }) => {
+ if (!pushpin[Highlight] && this.map_pinHighlighted.get(pushpin)) {
+ this.recolorPin(pushpin);
+ this.map_pinHighlighted.delete(pushpin);
+ }
+ });
+ allConfigPins.forEach(({ doc, pushpin }) => {
+ if (doc[Highlight] && !this.map_pinHighlighted.get(pushpin)) {
+ this.recolorPin(pushpin, 'orange');
+ this.map_pinHighlighted.set(pushpin, true);
+ }
+ });
+ },
+ { fireImmediately: true }
+ );
+
+ this._disposers.location = reaction(
+ () => ({ lat: this.rootDoc.latitude, lng: this.rootDoc.longitude, zoom: this.rootDoc.map_zoom, mapType: this.rootDoc.map_type }),
+ locationObject => {
+ // if (this._bingMap.current)
+ try {
+ locationObject?.zoom &&
+ this._bingMap.current?.setView({
+ mapTypeId: locationObject.mapType,
+ zoom: locationObject.zoom,
+ center: new this.MicrosoftMaps.Location(locationObject.lat, locationObject.lng),
+ });
+ } catch (e) {
+ console.log(e);
+ }
+ },
+ { fireImmediately: true }
+ );
+ };
+
+ dragToggle = (e: React.PointerEvent) => {
+ let dragClone: HTMLDivElement | undefined;
+
+ setupMoveUpEvents(
+ e,
+ e,
+ e => {
+ if (!dragClone) {
+ dragClone = this._dragRef.current?.cloneNode(true) as HTMLDivElement;
+ dragClone.style.position = 'absolute';
+ dragClone.style.zIndex = '10000';
+ DragManager.Root().appendChild(dragClone);
+ }
+ dragClone.style.transform = `translate(${e.clientX - 15}px, ${e.clientY - 15}px)`;
+ return false;
+ },
+ e => {
+ if (!dragClone) return;
+ DragManager.Root().removeChild(dragClone);
+ let target = document.elementFromPoint(e.x, e.y);
+ while (target) {
+ 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 location = this._bingMap.current.tryPixelToLocation(new this.MicrosoftMaps.Point(x, y));
+ this.createPushpin(location.latitude, location.longitude);
+ break;
+ }
+ target = target.parentElement;
+ }
+ },
+ e => {
+ const createPin = () => this.createPushpin(this.rootDoc.latitude, this.rootDoc.longitude, this.rootDoc.map);
+ if (this.bingSearchBarContents) {
+ this.bingSearch().then(createPin);
+ } else createPin();
+ }
+ );
+ };
+
+ searchbarKeyDown = (e: any) => e.key === 'Enter' && this.bingSearch();
+
+ static _firstRender = true;
+ static _rerenderDelay = 500;
+ _rerenderTimeout: any;
+ render() {
+ // bcz: no idea what's going on here, but bings maps have some kind of bug
+ // such that we need to delay rendering a second map on startup until the first map is rendered.
+ this.rootDoc[DocCss];
+ if (MapBoxContainer._rerenderDelay) {
+ // prettier-ignore
+ this._rerenderTimeout = this._rerenderTimeout ??
+ setTimeout(action(() => {
+ if ((window as any).Microsoft?.Maps?.Internal._WorkDispatcher) {
+ MapBoxContainer._rerenderDelay = 0;
+ }
+ this._rerenderTimeout = undefined;
+ this.rootDoc[DocCss] = this.rootDoc[DocCss] + 1;
+ }), MapBoxContainer._rerenderDelay);
+ return null;
+ }
+
+ const renderAnnotations = (childFilters?: () => string[]) => null;
+ return (
+ <div className="mapBox" ref={this._ref}>
+ <div
+ className="mapBox-wrapper"
+ onWheel={e => e.stopPropagation()}
+ onPointerDown={async e => {
+ e.button === 0 && !e.ctrlKey && e.stopPropagation();
+ }}
+ style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}>
+ <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>
+ {renderAnnotations(this.opaqueFilter)}
+ {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"
+ />
+ <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
+ fill="currentColor"
+ d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"></path>
+ </svg>
+ }
+ onClick={this.bingSearch}
+ type={Type.TERT}
+ />
+ <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>
+ <MapProvider>
+ <MapboxMap
+ id="mabox-map"
+ mapStyle={`mapbox://styles/mapbox/streets-v9`}
+ mapboxAccessToken={mapboxApiKey}
+ />
+ </MapProvider>
+
+
+{/*
+ <BingMapsReact
+ onMapReady={this.bingMapReady} //
+ bingMapsKey={bingApiKey}
+ height="100%"
+ mapOptions={this.bingMapOptions}
+ width="100%"
+ viewOptions={this.bingViewOptions}
+ /> */}
+ <div>
+ {!this._mapReady
+ ? null
+ : this.allAnnotations
+ .filter(anno => !anno.layout_unrendered)
+ .map((pushpin, i) => (
+ <DocumentView
+ key={i}
+ {...this.props}
+ renderDepth={this.props.renderDepth + 1}
+ Document={pushpin}
+ DataDoc={undefined}
+ PanelWidth={returnOne}
+ PanelHeight={returnOne}
+ NativeWidth={returnOne}
+ NativeHeight={returnOne}
+ onKey={undefined}
+ onDoubleClick={undefined}
+ onBrowseClick={undefined}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ isDocumentActive={returnFalse}
+ isContentActive={returnFalse}
+ addDocTab={returnFalse}
+ ScreenToLocalTransform={Transform.Identity}
+ fitContentsToBox={undefined}
+ focus={returnOne}
+ />
+ ))}
+ </div>
+ {/* <MapBoxInfoWindow
+ key={Docs.Create.MapMarkerDocument(NumCast(40), NumCast(40), false, [], {})[Id]}
+ {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ place={Docs.Create.MapMarkerDocument(NumCast(40), NumCast(40), false, [], {})}
+ markerMap={this.markerMap}
+ PanelWidth={this.infoWidth}
+ PanelHeight={this.infoHeight}
+ moveDocument={this.moveDocument}
+ isAnyChildContentActive={this.isAnyChildContentActive}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ /> */}
+ </div>
+ {/* </LoadScript > */}
+ <div className="mapBox-sidebar" style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
+ <SidebarAnnos
+ ref={this._sidebarRef}
+ {...this.props}
+ fieldKey={this.fieldKey}
+ rootDoc={this.rootDoc}
+ layoutDoc={this.layoutDoc}
+ dataDoc={this.dataDoc}
+ usePanelWidth={true}
+ showSidebar={this.SidebarShown}
+ nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ PanelWidth={this.sidebarWidth}
+ sidebarAddDocument={this.sidebarAddDocument}
+ moveDocument={this.moveDocument}
+ removeDocument={this.sidebarRemoveDocument}
+ />
+ </div>
+ {this.sidebarHandle}
+ </div>
+ );
+ }
+}
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 24c18c232..084cb872a 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -816,7 +816,7 @@ export namespace Doc {
}
export function FindReferences(infield: Doc | List<any>, references: Set<Doc>, system: boolean | undefined) {
- if (infield instanceof List<any>) {
+ if (!(infield instanceof Doc)) {
infield.forEach(val => (val instanceof Doc || val instanceof List) && FindReferences(val, references, system));
return;
}