aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2023-12-15 11:16:09 -0500
committerbobzel <zzzman@gmail.com>2023-12-15 11:16:09 -0500
commitc7efb74813244585f6805895e8e5fb5324fd8ed5 (patch)
tree0424aaf8d52e6d60435f6b1ea17b477a06d3359f
parent3ddfe7d4d56e62398d0f798409318f1a4715c275 (diff)
parente9b59a2790006af60206af7f676ad4a0ccceee13 (diff)
Merge remote-tracking branch 'origin/zaul-new-branch' into moreUpgrading
-rw-r--r--deploy/index.html1
-rw-r--r--src/client/documents/DocumentTypes.ts1
-rw-r--r--src/client/documents/Documents.ts27
-rw-r--r--src/client/views/AntimodeMenu.scss5
-rw-r--r--src/client/views/AntimodeMenu.tsx6
-rw-r--r--src/client/views/MainView.tsx2
-rw-r--r--src/client/views/nodes/MapBox/AnimationSpeedIcons.tsx35
-rw-r--r--src/client/views/nodes/MapBox/AnimationUtility.ts429
-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.scss79
-rw-r--r--src/client/views/nodes/MapBox/MapAnchorMenu.tsx401
-rw-r--r--src/client/views/nodes/MapBox/MapBox.scss118
-rw-r--r--src/client/views/nodes/MapBox/MapBox.tsx1188
-rw-r--r--src/client/views/nodes/MapBox/MapboxApiUtility.ts105
-rw-r--r--src/client/views/nodes/MapBox/MarkerIcons.tsx76
-rw-r--r--src/client/views/nodes/MapBox/icon_images/mapbox-marker-icon-20px-blue.pngbin0 -> 1623 bytes
-rw-r--r--src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx844
18 files changed, 3402 insertions, 159 deletions
diff --git a/deploy/index.html b/deploy/index.html
index b345e193f..186c217d8 100644
--- a/deploy/index.html
+++ b/deploy/index.html
@@ -1,6 +1,7 @@
<html style="overflow: hidden">
<head>
<title>Dash</title>
+ <link href='https://api.tiles.mapbox.com/mapbox-gl-js/v3.0.1/mapbox-gl.css' rel='stylesheet' />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 427b11751..8cd36b312 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -36,6 +36,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 dd6d6cdf6..678c0daf8 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -176,9 +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?: 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');
_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');
@@ -766,6 +771,13 @@ export namespace Docs {
options: {},
},
],
+ [
+ DocumentType.MAPROUTE,
+ {
+ layout: { view: CollectionView, dataField: defaultDataKey },
+ options: {},
+ },
+ ],
]);
const suffix = 'Proto';
@@ -1112,10 +1124,21 @@ 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 });
@@ -1976,4 +1999,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/AntimodeMenu.scss b/src/client/views/AntimodeMenu.scss
index 4613cec76..2ebf673fe 100644
--- a/src/client/views/AntimodeMenu.scss
+++ b/src/client/views/AntimodeMenu.scss
@@ -14,6 +14,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 4c82b10fd..db7e64deb 100644
--- a/src/client/views/AntimodeMenu.tsx
+++ b/src/client/views/AntimodeMenu.tsx
@@ -143,10 +143,12 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends Observab
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/MainView.tsx b/src/client/views/MainView.tsx
index 636f8761f..655e34592 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -72,6 +72,7 @@ import { AnchorMenu } from './pdf/AnchorMenu';
import { GPTPopup } from './pdf/GPTPopup/GPTPopup';
import { TopBar } from './topbar/TopBar';
const { default: { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
+import { DirectionsAnchorMenu } from './nodes/MapBox/DirectionsAnchorMenu';
const _global = (window /* browser */ || global) /* node */ as any;
@observer
@@ -1056,6 +1057,7 @@ export class MainView extends ObservableReactComponent<{}> {
<RadialMenu />
<AnchorMenu />
<MapAnchorMenu />
+ <DirectionsAnchorMenu />
<DashFieldViewMenu />
<MarqueeOptionsMenu />
<TimelineMenu />
diff --git a/src/client/views/nodes/MapBox/AnimationSpeedIcons.tsx b/src/client/views/nodes/MapBox/AnimationSpeedIcons.tsx
new file mode 100644
index 000000000..d54a175b2
--- /dev/null
+++ b/src/client/views/nodes/MapBox/AnimationSpeedIcons.tsx
@@ -0,0 +1,35 @@
+import * as React from "react";
+
+export const slowSpeedIcon: JSX.Element = (
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 435.62">
+ <defs>
+ <style type="text/css">
+ {`
+ .fil0 { fill: black; fill-rule: nonzero; }
+ .fil1 { fill: #FE0000; fill-rule: nonzero; }
+ `}
+ </style>
+ </defs>
+ <path className="fil0" d="M174.84 343.06c-7.31,-13.12 -13.03,-27.28 -16.89,-42.18 -3.76,-14.56 -5.77,-29.71 -5.77,-45.17 0,-11.94 1.19,-23.66 3.43,-35.03 2.29,-11.57 5.74,-22.83 10.2,-33.63 13.7,-33.14 37.01,-61.29 66.42,-80.96 25.38,-16.96 55.28,-27.66 87.45,-29.87l0 -30.17c0,-0.46 0.02,-0.92 0.06,-1.37l-33.7 0c-5.53,0 -10.05,-4.52 -10.05,-10.04l0 -24.59c0,-5.53 4.52,-10.05 10.05,-10.05l101.27 0c5.53,0 10.05,4.52 10.05,10.05l0 24.59c0,5.52 -4.52,10.04 -10.05,10.04l-33.69 0c0.03,0.45 0.05,0.91 0.05,1.37l0 31.03 -0.1 0c41.1,4.89 77.94,23.63 105.73,51.42 32.56,32.55 52.7,77.54 52.7,127.21 0,49.67 -20.14,94.66 -52.7,127.21 -32.55,32.55 -77.54,52.7 -127.21,52.7 -33.16,0 -64.29,-9.04 -91.05,-24.78 -27.66,-16.27 -50.59,-39.73 -66.2,-67.78zm148.42 -36.62l-80.33 0 0 -25.71 28.6 0 0 -42.57 -28.6 1.93 0 -25.71 36.95 -8.35 25.38 0 0 74.7 18 0 0 25.71zm44.34 -100.41l11.08 26.83 1.61 0 11.09 -26.83 34.86 0 -22.33 48.52 22.33 51.89 -35.67 0 -12.05 -28.92 -1.44 0 -11.89 28.92 -34.06 0 21.85 -50.93 -21.85 -49.48 36.47 0zm126.08 -74.6c6.98,-16.66 6.15,-34.13 -3.84,-45.82 -12,-14.03 -33.67,-15.64 -53.8,-5.77 21.32,14.62 40.68,31.63 57.64,51.59zm-323.17 0c-6.98,-16.66 -6.16,-34.13 3.84,-45.82 11.99,-14.03 33.67,-15.64 53.79,-5.77 -21.32,14.62 -40.68,31.63 -57.63,51.59zm15.31 162.23c3.23,12.5 8.04,24.39 14.18,35.42 13.13,23.58 32.39,43.29 55.6,56.94 22.37,13.16 48.52,20.71 76.49,20.71 41.71,0 79.47,-16.9 106.8,-44.23 27.32,-27.32 44.23,-65.08 44.23,-106.79 0,-41.71 -16.91,-79.47 -44.23,-106.8 -27.33,-27.32 -65.09,-44.23 -106.8,-44.23 -31.07,0 -59.91,9.34 -83.84,25.33 -24.74,16.54 -44.33,40.19 -55.82,67.98 -3.68,8.91 -6.56,18.35 -8.5,28.22 -1.87,9.49 -2.86,19.36 -2.86,29.5 0,13.24 1.65,25.96 4.75,37.95z"/>
+ <path className="fil1" d="M55.23 188.52c-7.98,0 -14.45,-6.47 -14.45,-14.44 0,-7.98 6.47,-14.45 14.45,-14.45l63.94 0c7.98,0 14.45,6.47 14.45,14.45 0,7.97 -6.47,14.44 -14.45,14.44l-63.94 0zm0.72 167.68c-7.97,0 -14.44,-6.47 -14.44,-14.45 0,-7.97 6.47,-14.45 14.44,-14.45l64.58 0c7.97,0 14.45,6.48 14.45,14.45 0,7.98 -6.48,14.45 -14.45,14.45l-64.58 0zm-41.5 -84.94c-7.98,0 -14.45,-6.47 -14.45,-14.45 0,-7.97 6.47,-14.44 14.45,-14.44l89.12 0c7.98,0 14.45,6.47 14.45,14.44 0,7.98 -6.47,14.45 -14.45,14.45l-89.12 0z"/>
+ </svg>
+);
+
+export const mediumSpeedIcon: JSX.Element = (
+ <svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 122.88 104.55">
+ <defs><style>{`.cls-1{fill:#fe0000;}`}</style></defs>
+ <path d="M42,82.34a42.82,42.82,0,0,1-4.05-10.13A43.2,43.2,0,0,1,76.72,18.29V11.05c0-.11,0-.22,0-.33H68.65a2.41,2.41,0,0,1-2.41-2.41V2.41A2.41,2.41,0,0,1,68.65,0H93a2.42,2.42,0,0,1,2.42,2.41v5.9A2.42,2.42,0,0,1,93,10.72H84.87c0,.11,0,.22,0,.33V18.5h0A43.17,43.17,0,1,1,42,82.34ZM88.22,49.45l2.66,6.44h.39l2.66-6.44h8.37L96.94,61.09l5.36,12.45H93.74L90.85,66.6H90.5l-2.85,6.94H79.47l5.25-12.22L79.47,49.45ZM58.65,56.08l-1-5.75a33.58,33.58,0,0,1,9.68-1.46c1.28,0,2.35,0,3.22.11a11.77,11.77,0,0,1,2.67.58,5.41,5.41,0,0,1,2.2,1.28c1.24,1.23,1.85,3.12,1.85,5.66s-.72,4.42-2.16,5.63S70.64,64.73,66,66.3v1.08H76.89v6.16H57.11V68.72a10.73,10.73,0,0,1,.81-4.12,8.4,8.4,0,0,1,2.43-2.7,12.13,12.13,0,0,1,2.79-1.7l3.32-1.52c1-.47,1.88-.87,2.52-1.17V55.42a28.59,28.59,0,0,0-3.2-.19,30.66,30.66,0,0,0-7.13.85Zm59.83-24.54c1.68-4,1.48-8.19-.92-11-2.88-3.37-8.08-3.76-12.91-1.39a69.74,69.74,0,0,1,13.83,12.38Zm-77.56,0c-1.67-4-1.48-8.19.92-11,2.88-3.37,8.08-3.76,12.91-1.39A70,70,0,0,0,40.92,31.54ZM44.6,70.48A36,36,0,0,0,48,79a35.91,35.91,0,1,0-3.4-8.5Z"/>
+ <path className="cls-1" d="M13.25,45.25a3.47,3.47,0,0,1,0-6.94H28.6a3.47,3.47,0,0,1,0,6.94Z"/>
+ <path className="cls-1" d="M3.47,65.1a3.47,3.47,0,1,1,0-6.93H24.86a3.47,3.47,0,0,1,0,6.93Z"/>
+ <path className="cls-1" d="M13.43,85.49a3.47,3.47,0,1,1,0-6.94h15.5a3.47,3.47,0,0,1,0,6.94Z"/>
+ </svg>
+);
+
+export const fastSpeedIcon: JSX.Element = (
+ <svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 122.88 104.55">
+ <defs><style>{`.cls-1{fill:#fe0000;`}</style></defs>
+ <path d="M42,82.34a42.82,42.82,0,0,1-4.05-10.13A43.2,43.2,0,0,1,76.72,18.29V11.05c0-.11,0-.22,0-.33H68.65a2.41,2.41,0,0,1-2.41-2.41V2.41A2.41,2.41,0,0,1,68.65,0H93a2.42,2.42,0,0,1,2.42,2.41v5.9A2.42,2.42,0,0,1,93,10.72H84.87c0,.11,0,.22,0,.33V18.5h0A43.17,43.17,0,1,1,42,82.34ZM88.22,49.61l2.66,6.44h.39l2.66-6.44h8.37L96.94,61.26l5.36,12.45H93.74l-2.9-6.94H90.5l-2.86,6.94H79.47l5.24-12.22L79.47,49.61Zm-19,8.48v-2.5a24.92,24.92,0,0,0-3.74-.2A33.25,33.25,0,0,0,59,56.2l-1-5.7A30.47,30.47,0,0,1,67.13,49a22.86,22.86,0,0,1,5.48.47,6.91,6.91,0,0,1,2.5,1.11,5.62,5.62,0,0,1,1.78,4.55,5.84,5.84,0,0,1-3.2,5.56v.19a5.73,5.73,0,0,1,3.81,5.74,8.67,8.67,0,0,1-.63,3.49,6,6,0,0,1-1.6,2.24,7.15,7.15,0,0,1-2.55,1.25,25.64,25.64,0,0,1-6.61.66,37.78,37.78,0,0,1-8.54-1l1.08-6.37a27.22,27.22,0,0,0,6.21.89,35.79,35.79,0,0,0,4.35-.23V65.11l-6.63-.65V58.87l6.63-.78Zm49.27-26.55c1.68-4,1.48-8.19-.92-11-2.88-3.37-8.08-3.76-12.91-1.39a69.74,69.74,0,0,1,13.83,12.38Zm-77.56,0c-1.67-4-1.48-8.19.92-11,2.88-3.37,8.08-3.76,12.91-1.39A70,70,0,0,0,40.92,31.54ZM44.6,70.48A36,36,0,0,0,48,79a35.91,35.91,0,1,0-3.4-8.5Z"/>
+ <path className="cls-1" d="M13.25,45.25a3.47,3.47,0,0,1,0-6.94H28.6a3.47,3.47,0,0,1,0,6.94Zm.18,40.24a3.47,3.47,0,1,1,0-6.94h15.5a3.47,3.47,0,0,1,0,6.94ZM3.47,65.1a3.47,3.47,0,1,1,0-6.93H24.86a3.47,3.47,0,0,1,0,6.93Z"/>
+ </svg>
+);
+
diff --git a/src/client/views/nodes/MapBox/AnimationUtility.ts b/src/client/views/nodes/MapBox/AnimationUtility.ts
new file mode 100644
index 000000000..256acbf13
--- /dev/null
+++ b/src/client/views/nodes/MapBox/AnimationUtility.ts
@@ -0,0 +1,429 @@
+import mapboxgl from "mapbox-gl";
+import { MercatorCoordinate } from "mapbox-gl";
+import { MapRef } from "react-map-gl";
+import * as React from 'react';
+import * as d3 from "d3";
+import * as turf from '@turf/turf';
+import { Position } from "@turf/turf";
+import { Feature, FeatureCollection, GeoJsonProperties, Geometry } from "geojson";
+import { observer } from "mobx-react";
+import { action, computed, observable } from "mobx";
+
+export enum AnimationStatus {
+ START = 'start',
+ RESUME = 'resume',
+ RESTART = 'restart',
+}
+
+export enum AnimationSpeed {
+ SLOW = '1x',
+ MEDIUM = '2x',
+ FAST = '3x',
+}
+
+@observer
+export class AnimationUtility {
+ private SMOOTH_FACTOR = 0.95
+ private ROUTE_COORDINATES: Position[] = [];
+ private PATH: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties>;
+ private FLY_IN_START_PITCH = 40;
+ private FIRST_LNG_LAT: {lng: number, lat: number};
+ private START_ALTITUDE = 3_000_000;
+
+ @observable private isStreetViewAnimation: boolean;
+ @observable private animationSpeed: AnimationSpeed;
+
+
+ private previousLngLat: {lng: number, lat: number};
+
+ private currentStreetViewBearing: number = 0;
+
+ @computed get flyInEndBearing() {
+ return this.isStreetViewAnimation ?
+ this.calculateBearing(
+ {
+ lng: this.ROUTE_COORDINATES[0][0],
+ lat: this.ROUTE_COORDINATES[0][1]
+ },
+ {
+ lng: this.ROUTE_COORDINATES[1][0],
+ lat: this.ROUTE_COORDINATES[1][1]
+ }
+ )
+ : -20;
+ }
+
+ @computed get flyInStartBearing() {
+ return Math.max(0, Math.min(this.flyInEndBearing + 20, 360)); // between 0 and 360
+ }
+
+ @computed get flyInEndAltitude() {
+ return this.isStreetViewAnimation ? 70 : 10000;
+ }
+
+ @computed get flyInEndPitch() {
+ return this.isStreetViewAnimation ? 80 : 50;
+ }
+
+ @computed get flyToDuration() {
+ switch (this.animationSpeed) {
+ case AnimationSpeed.SLOW:
+ return 4000;
+ case AnimationSpeed.MEDIUM:
+ return 2500;
+ case AnimationSpeed.FAST:
+ return 1250;
+ default:
+ return 2500;
+ }
+ }
+
+ @computed get animationDuration(): number {
+ return 20_000;
+ // compute path distance for a standard speed
+ // get animation speed
+ // get isStreetViewAnimation (should be slower if so)
+ }
+
+ @action
+ public updateAnimationSpeed(speed: AnimationSpeed) {
+ // calculate new flyToDuration and animationDuration
+ this.animationSpeed = speed;
+ }
+
+ @action
+ public updateIsStreetViewAnimation(isStreetViewAnimation: boolean) {
+ this.isStreetViewAnimation = isStreetViewAnimation;
+ }
+
+
+ constructor(
+ firstLngLat: {lng: number, lat: number},
+ routeCoordinates: Position[],
+ isStreetViewAnimation: boolean,
+ animationSpeed: AnimationSpeed
+ ) {
+ this.FIRST_LNG_LAT = firstLngLat;
+ this.previousLngLat = firstLngLat;
+ this.ROUTE_COORDINATES = routeCoordinates;
+ this.PATH = turf.lineString(routeCoordinates);
+
+ const bearing = this.calculateBearing(
+ {
+ lng: routeCoordinates[0][0],
+ lat: routeCoordinates[0][1]
+ },
+ {
+ lng: routeCoordinates[1][0],
+ lat: routeCoordinates[1][1]
+ }
+ );
+ this.currentStreetViewBearing = bearing;
+ // if (isStreetViewAnimation){
+ // this.flyInEndBearing = bearing;
+ // }
+ this.isStreetViewAnimation = isStreetViewAnimation;
+ this.animationSpeed = animationSpeed;
+ // calculate animation duration based on speed
+ // this.animationDuration = animationDuration;
+ }
+
+ public animatePath = async ({
+ map,
+ // path,
+ // startBearing,
+ // startAltitude,
+ // pitch,
+ currentAnimationPhase,
+ updateAnimationPhase,
+ updateFrameId,
+ }: {
+ map: MapRef,
+ // path: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties>,
+ // startBearing: number,
+ // startAltitude: number,
+ // pitch: number,
+ currentAnimationPhase: number,
+ updateAnimationPhase: (
+ newAnimationPhase: number,
+ ) => void,
+ updateFrameId: (newFrameId: number) => void;
+
+ }) => {
+ return new Promise<void>(async (resolve) => {
+ const pathDistance = turf.lineDistance(this.PATH);
+ console.log("PATH DISTANCE: ", pathDistance);
+ let startTime: number | null = null;
+
+ const frame = async (currentTime: number) => {
+ if (!startTime) startTime = currentTime;
+ const elapsedSinceLastFrame = currentTime - startTime;
+ const phaseIncrement = elapsedSinceLastFrame / this.animationDuration;
+ const animationPhase = currentAnimationPhase + phaseIncrement;
+
+ // when the duration is complete, resolve the promise and stop iterating
+ if (animationPhase > 1) {
+ resolve();
+ return;
+ }
+
+
+ // calculate the distance along the path based on the animationPhase
+ const alongPath = turf.along(this.PATH, pathDistance * animationPhase).geometry
+ .coordinates;
+
+ const lngLat = {
+ lng: alongPath[0],
+ lat: alongPath[1],
+ };
+
+ let bearing: number;
+ if (this.isStreetViewAnimation){
+ bearing = this.lerp(
+ this.currentStreetViewBearing,
+ this.calculateBearing(this.previousLngLat, lngLat),
+ 0.028 // Adjust the transition speed as needed
+ );
+ this.currentStreetViewBearing = bearing;
+ // bearing = this.calculateBearing(this.previousLngLat, lngLat); // TODO: Calculate actual bearing
+ } else {
+ // slowly rotate the map at a constant rate
+ bearing = this.flyInEndBearing - animationPhase * 200.0;
+ // bearing = startBearing - animationPhase * 200.0;
+ }
+
+ this.previousLngLat = lngLat;
+
+ updateAnimationPhase(animationPhase);
+
+ // compute corrected camera ground position, so that he leading edge of the path is in view
+ var correctedPosition = this.computeCameraPosition(
+ this.isStreetViewAnimation,
+ this.flyInEndPitch,
+ bearing,
+ lngLat,
+ this.flyInEndAltitude,
+ true // smooth
+ );
+
+ // set the pitch and bearing of the camera
+ const camera = map.getFreeCameraOptions();
+ camera.setPitchBearing(this.flyInEndPitch, bearing);
+
+ console.log("Corrected pos: ", correctedPosition);
+ console.log("Start altitude: ", this.flyInEndAltitude);
+ // set the position and altitude of the camera
+ camera.position = MercatorCoordinate.fromLngLat(
+ correctedPosition,
+ this.flyInEndAltitude
+ );
+
+
+ // apply the new camera options
+ map.setFreeCameraOptions(camera);
+
+ // repeat!
+ const innerFrameId = await window.requestAnimationFrame(frame);
+ updateFrameId(innerFrameId);
+ };
+
+ const outerFrameId = await window.requestAnimationFrame(frame);
+ updateFrameId(outerFrameId);
+ });
+ };
+
+ public flyInAndRotate = async ({
+ map,
+ updateFrameId
+ }:
+ {
+ map: MapRef,
+ updateFrameId: (newFrameId: number) => void
+ }
+ ) => {
+ return new Promise<{bearing: number, altitude: number}>(async (resolve) => {
+ let start: number | null;
+
+ var currentAltitude;
+ var currentBearing;
+ var currentPitch;
+
+ // the animation frame will run as many times as necessary until the duration has been reached
+ const frame = async (time: number) => {
+ if (!start) {
+ start = time;
+ }
+
+ // otherwise, use the current time to determine how far along in the duration we are
+ let animationPhase = (time - start) / this.flyToDuration;
+
+ // because the phase calculation is imprecise, the final zoom can vary
+ // if it ended up greater than 1, set it to 1 so that we get the exact endAltitude that was requested
+ if (animationPhase > 1) {
+ animationPhase = 1;
+ }
+
+ currentAltitude = this.START_ALTITUDE + (this.flyInEndAltitude - this.START_ALTITUDE) * d3.easeCubicOut(animationPhase)
+ // rotate the camera between startBearing and endBearing
+ currentBearing = this.flyInStartBearing + (this.flyInEndBearing - this.flyInStartBearing) * d3.easeCubicOut(animationPhase)
+
+ currentPitch = this.FLY_IN_START_PITCH + (this.flyInEndPitch - this.FLY_IN_START_PITCH) * d3.easeCubicOut(animationPhase)
+
+ // compute corrected camera ground position, so the start of the path is always in view
+ var correctedPosition = this.computeCameraPosition(
+ false,
+ currentPitch,
+ currentBearing,
+ this.FIRST_LNG_LAT,
+ currentAltitude
+ );
+
+ // set the pitch and bearing of the camera
+ const camera = map.getFreeCameraOptions();
+ camera.setPitchBearing(currentPitch, currentBearing);
+
+ // set the position and altitude of the camera
+ camera.position = MercatorCoordinate.fromLngLat(
+ correctedPosition,
+ currentAltitude
+ );
+
+ // apply the new camera options
+ map.setFreeCameraOptions(camera);
+
+ // when the animationPhase is done, resolve the promise so the parent function can move on to the next step in the sequence
+ if (animationPhase === 1) {
+ resolve({
+ bearing: currentBearing,
+ altitude: currentAltitude,
+ });
+
+ // return so there are no further iterations of this frame
+ return;
+ }
+
+ const innerFrameId = await window.requestAnimationFrame(frame);
+ updateFrameId(innerFrameId);
+
+ };
+
+ const outerFrameId = await window.requestAnimationFrame(frame);
+ updateFrameId(outerFrameId);
+ });
+ };
+
+ previousCameraPosition: {lng: number, lat: number} | null = null;
+
+ lerp = (start: number, end: number, amt: number) => {
+ return (1 - amt) * start + amt * end;
+ }
+
+ computeCameraPosition = (
+ isStreetViewAnimation: boolean,
+ pitch: number,
+ bearing: number,
+ targetPosition: {lng: number, lat: number},
+ altitude: number,
+ smooth = false
+ ) => {
+ const bearingInRadian = (bearing * Math.PI) / 180;
+ const pitchInRadian = ((90 - pitch)* Math.PI) / 180;
+
+ let correctedLng = targetPosition.lng;
+ let correctedLat = targetPosition.lat;
+
+ if (!isStreetViewAnimation) {
+ const lngDiff =
+ ((altitude / Math.tan(pitchInRadian)) *
+ Math.sin(-bearingInRadian)) /
+ 70000; // ~70km/degree longitude
+ const latDiff =
+ ((altitude / Math.tan(pitchInRadian)) *
+ Math.cos(-bearingInRadian)) /
+ 110000 // 110km/degree latitude
+
+ correctedLng = targetPosition.lng + lngDiff;
+ correctedLat = targetPosition.lat - latDiff;
+
+ }
+
+ const newCameraPosition = {
+ lng: correctedLng,
+ lat: correctedLat
+ };
+
+ if (smooth) {
+ if (this.previousCameraPosition) {
+ newCameraPosition.lng = this.lerp(newCameraPosition.lng, this.previousCameraPosition.lng, this.SMOOTH_FACTOR);
+ newCameraPosition.lat = this.lerp(newCameraPosition.lat, this.previousCameraPosition.lat, this.SMOOTH_FACTOR);
+ }
+ }
+
+ this.previousCameraPosition = newCameraPosition
+
+ return newCameraPosition;
+ };
+
+ public static createGeoJSONCircle = (center: number[], radiusInKm: number, points = 64): Feature<Geometry, GeoJsonProperties>=> {
+ const coords = {
+ latitude: center[1],
+ longitude: center[0],
+ };
+ const km = radiusInKm;
+ const ret = [];
+ const distanceX = km / (111.320 * Math.cos((coords.latitude * Math.PI) / 180));
+ const distanceY = km / 110.574;
+ let theta;
+ let x;
+ let y;
+ for (let i = 0; i < points; i += 1) {
+ theta = (i / points) * (2 * Math.PI);
+ x = distanceX * Math.cos(theta);
+ y = distanceY * Math.sin(theta);
+ ret.push([coords.longitude + x, coords.latitude + y]);
+ }
+ ret.push(ret[0]);
+ return {
+ type: 'Feature',
+ geometry: {
+ type: 'Polygon',
+ coordinates: [ret],
+ },
+ properties: {}
+ };
+ }
+
+ private calculateBearing(
+ from: { lng: number; lat: number },
+ to: { lng: number; lat: number }
+ ): number {
+ const lon1 = from.lng;
+ const lat1 = from.lat;
+ const lon2 = to.lng;
+ const lat2 = to.lat;
+
+ const lon1Rad = (lon1 * Math.PI) / 180;
+ const lon2Rad = (lon2 * Math.PI) / 180;
+ const lat1Rad = (lat1 * Math.PI) / 180;
+ const lat2Rad = (lat2 * Math.PI) / 180;
+
+ const y = Math.sin(lon2Rad - lon1Rad) * Math.cos(lat2Rad);
+ const x =
+ Math.cos(lat1Rad) * Math.sin(lat2Rad) -
+ Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(lon2Rad - lon1Rad);
+
+ let bearing = Math.atan2(y,x);
+
+ // Convert bearing from radians to degrees
+ bearing = (bearing * 180) / Math.PI;
+
+ // Ensure the bearing is within [0, 360)
+ if (bearing < 0) {
+ bearing += 360;
+ }
+
+ return bearing;
+ }
+
+
+} \ No newline at end of file
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..c36d98afe 100644
--- a/src/client/views/nodes/MapBox/MapAnchorMenu.scss
+++ b/src/client/views/nodes/MapBox/MapAnchorMenu.scss
@@ -51,4 +51,81 @@
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;
+ }
+
+
+ }
+
+ .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 66c4dc7b8..b1fb3368c 100644
--- a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx
+++ b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx
@@ -1,15 +1,27 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { IReactionDisposer, ObservableMap, makeObservable, reaction } from 'mobx';
-import { observer } from 'mobx-react';
import * as React from 'react';
+import { IReactionDisposer, ObservableMap, action, makeObservable, observable, reaction, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import { Doc, NumListCast, Opt } from '../../../../fields/Doc';
import { returnFalse, setupMoveUpEvents, unimplementedFunction } from '../../../../Utils';
-import { Doc, Opt } from '../../../../fields/Doc';
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';
+import { MarkerIcons } from './MarkerIcons';
+import { CirclePicker, ColorResult } from 'react-color';
+import { Position } from 'geojson';
+
+type MapAnchorMenuType = 'standard' | 'routeCreation' | 'calendar' | 'customize' | 'route';
@observer
export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@@ -17,6 +29,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 +43,50 @@ 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: Position[], origin: string, destination: any, createPinForDestination: boolean) => void = unimplementedFunction;
+ public CreatePin: (feature: any) => void = unimplementedFunction;
+
+ public UpdateMarkerColor: (color: string) => void = unimplementedFunction;
+ public UpdateMarkerIcon: (iconKey: string) => void = unimplementedFunction;
+
+ public OpenAnimationPanel: (routeDoc: Doc | undefined) => void = unimplementedFunction;
+
+ @observable
+ menuType: MapAnchorMenuType = 'standard';
+
+ @action
+ public setMenuType = (menuType: MapAnchorMenuType) => {
+ this.menuType = menuType;
+ };
+
+ private allMapPinDocs: Doc[] = [];
+
+ private pinDoc: Doc | undefined = undefined;
+
+ private routeDoc: Doc | undefined = undefined;
+
+ private title: string | undefined = undefined;
+
+ public setPinDoc(pinDoc: Doc) {
+ this.pinDoc = pinDoc;
+ this.title = StrCast(pinDoc.title ? pinDoc.title : `${NumCast(pinDoc.longitude)}, ${NumCast(pinDoc.latitude)}`);
+ }
+
+ public setRouteDoc(routeDoc: Doc) {
+ this.routeDoc = routeDoc;
+ this.title = StrCast(routeDoc.title ?? 'Map route');
+ }
+
+ 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 +99,10 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
}
componentWillUnmount() {
+ this.destinationFeatures = [];
+ this.destinationSelected = false;
+ this.selectedDestinationFeature = undefined;
+ this.currentRouteInfoMap = undefined;
this._disposer?.();
}
@@ -81,39 +142,224 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
};
static top = React.createRef<HTMLDivElement>();
+
// public get Top(){
// return this.top
// }
+ @action
+ DirectionsClick = () => {
+ this.menuType = 'routeCreation';
+ };
+
+ @action
+ CustomizeClick = () => {
+ this.currentRouteInfoMap = undefined;
+ this.menuType = 'customize';
+ };
+
+ @action
+ BackClick = () => {
+ this.currentRouteInfoMap = undefined;
+ this.menuType = 'standard';
+ };
+
+ @action
+ TriggerFileInputClick = () => {
+ if (this._fileInputRef) {
+ this._fileInputRef.current?.click(); // Trigger the file input click event
+ }
+ };
+
+ @action
+ onMarkerColorChange = (color: ColorResult) => {
+ if (this.pinDoc) {
+ this.pinDoc.markerColor = color.hex;
+ }
+ };
+
+ revertToOriginalMarker = () => {
+ if (this.pinDoc) {
+ this.pinDoc.markerType = 'MAP_PIN';
+ this.pinDoc.markerColor = '#ff5722';
+ }
+ };
+
+ onMarkerIconChange = (iconKey: string) => {
+ if (this.pinDoc) {
+ this.pinDoc.markerType = iconKey;
+ }
+ };
+
+ @observable
+ destinationFeatures: any[] = [];
+
+ @observable
+ destinationSelected: boolean = false;
+
+ @observable
+ selectedDestinationFeature: any = undefined;
+
+ @observable
+ createPinForDestination: boolean = true;
+
+ @observable
+ currentRouteInfoMap: Record<TransportationType, any> | undefined = undefined;
+
+ @observable
+ selectedTransportationType: TransportationType = 'driving';
+
+ @action
+ handleTransportationTypeChange = (newType: TransportationType) => {
+ if (newType !== this.selectedTransportationType) {
+ this.selectedTransportationType = newType;
+ this.DisplayRoute(this.currentRouteInfoMap, newType);
+ }
+ };
+
+ @action
+ handleSelectedDestinationFeature = (destinationFeature: any) => {
+ this.selectedDestinationFeature = destinationFeature;
+ };
+
+ @action
+ toggleCreatePinForDestinationCheckbox = () => {
+ this.createPinForDestination = !this.createPinForDestination;
+ };
+
+ @action
+ handleDestinationSearchChange = async (searchText: string) => {
+ if (this.selectedDestinationFeature !== undefined) this.selectedDestinationFeature = undefined;
+ const features = await MapboxApiUtility.forwardGeocodeForFeatures(searchText);
+ if (features) {
+ runInAction(() => {
+ this.destinationFeatures = features;
+ });
+ }
+ };
+
+ getRoutes = async (destinationFeature: any) => {
+ const currentPinLong: number = NumCast(this.pinDoc?.longitude);
+ const currentPinLat: number = NumCast(this.pinDoc?.latitude);
+
+ if (currentPinLong && currentPinLat && destinationFeature.center) {
+ const routeInfoMap = await MapboxApiUtility.getDirections([currentPinLong, currentPinLat], destinationFeature.center);
+ if (routeInfoMap) {
+ runInAction(() => {
+ this.currentRouteInfoMap = routeInfoMap;
+ });
+ this.DisplayRoute(routeInfoMap, 'driving');
+ }
+ }
+
+ // get route menu, set it equal to here
+ // create a temporary route
+ // create pin if createPinForDestination was clicked
+ };
+
+ HandleAddRouteClick = () => {
+ if (this.currentRouteInfoMap && this.selectedTransportationType && this.selectedDestinationFeature) {
+ const coordinates = this.currentRouteInfoMap[this.selectedTransportationType].coordinates;
+ console.log(coordinates);
+ console.log(this.selectedDestinationFeature);
+ this.AddNewRouteToMap(coordinates, this.title ?? '', this.selectedDestinationFeature, this.createPinForDestination);
+ this.HideRoute();
+ }
+ };
+
+ getMarkerIcon = (): JSX.Element | undefined => {
+ if (this.pinDoc) {
+ const markerType = StrCast(this.pinDoc.markerType);
+ const markerColor = StrCast(this.pinDoc.markerColor);
+
+ return MarkerIcons.getFontAwesomeIcon(markerType, '2x', markerColor);
+ }
+ return undefined;
+ };
+
render() {
const buttons = (
- <>
- {
- <IconButton
- tooltip="Delete Pin" //
- onPointerDown={this.Delete}
- icon={<FontAwesomeIcon icon="trash-alt" />}
- color={SettingsManager.userColor}
- />
- }
- {
- <div ref={this._commentRef}>
+ <div className="menu-buttons" style={{ display: 'flex' }}>
+ {this.menuType === 'standard' && (
+ <>
<IconButton
- tooltip="Link Note to Pin" //
- onPointerDown={this.notePointerDown}
- icon={<FontAwesomeIcon icon="sticky-note" />}
+ tooltip="Delete Pin" //
+ onPointerDown={this.Delete}
+ icon={<FontAwesomeIcon icon="trash-alt" />}
color={SettingsManager.userColor}
/>
- </div>
- }
- {
- <IconButton
- tooltip="Center on pin" //
- onPointerDown={this.Center}
- icon={<FontAwesomeIcon icon="compress-arrows-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}
+ />
+ </>
+ )}
+ {this.menuType === 'routeCreation' && (
+ <>
+ <IconButton
+ tooltip="Go back" //
+ onPointerDown={this.BackClick}
+ icon={<FontAwesomeIcon icon={faArrowLeft as IconLookup} />}
+ color={SettingsManager.userColor}
+ />
+ <IconButton
+ tooltip="Add route" //
+ onPointerDown={this.HandleAddRouteClick}
+ icon={<FontAwesomeIcon icon={faAdd as IconLookup} />}
+ color={SettingsManager.userColor}
+ />
+ </>
+ )}
+ {this.menuType === 'route' && (
+ <>
+ <IconButton
+ tooltip="Delete Route" //
+ onPointerDown={this.Delete}
+ icon={<FontAwesomeIcon icon="trash-alt" />}
+ color={SettingsManager.userColor}
+ />
+ <IconButton tooltip="Animate route" onPointerDown={() => this.OpenAnimationPanel(this.routeDoc)} /**TODO: fix */ icon={<FontAwesomeIcon icon={faRoute 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="Add to calendar" onPointerDown={this.Delete} /**TODO: fix */ icon={<FontAwesomeIcon icon={faCalendarDays as IconLookup} />} color={SettingsManager.userColor} />
+ </>
+ )}
+ {this.menuType === 'customize' && (
+ <>
+ <IconButton
+ tooltip="Go back" //
+ onPointerDown={this.BackClick}
+ icon={<FontAwesomeIcon icon={faArrowLeft as IconLookup} />}
+ color={SettingsManager.userColor}
+ />
+ <IconButton
+ tooltip="Revert to original" //
+ onPointerDown={() => this.revertToOriginalMarker()}
+ icon={<FontAwesomeIcon icon={faArrowsRotate as IconLookup} />}
+ color={SettingsManager.userColor}
+ />
+ </>
+ )}
+
{/* {this.IsTargetToggler !== returnFalse && (
<Toggle
tooltip={'Make target visibility toggle on click'}
@@ -125,13 +371,106 @@ 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 === 'routeCreation' && (
+ <div className="direction-inputs" style={{ display: 'flex', flexDirection: 'column' }}>
+ <TextField fullWidth disabled value={this.title} />
+ <FontAwesomeIcon icon={faArrowDown as IconLookup} size="xs" />
+ <Autocomplete
+ fullWidth
+ id="route-destination-searcher"
+ onInputChange={(e: any, searchText: any) => this.handleDestinationSearchChange(searchText)}
+ onChange={(e: any, feature: any, reason: any) => {
+ if (reason === 'clear') {
+ this.handleSelectedDestinationFeature(undefined);
+ } else if (reason === 'selectOption') {
+ this.handleSelectedDestinationFeature(feature);
+ }
+ }}
+ options={this.destinationFeatures.filter(feature => feature.place_name).map(feature => feature)}
+ getOptionLabel={(feature: any) => feature.place_name}
+ renderInput={(params: any) => <TextField {...params} placeholder="Enter a destination" />}
+ />
+ {this.selectedDestinationFeature && (
+ <>
+ {!this.allMapPinDocs.some(pinDoc => pinDoc.title === this.selectedDestinationFeature.place_name) && (
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '5px' }}>
+ <FormControlLabel label="Create pin for destination?" control={<Checkbox color="success" checked={this.createPinForDestination} onChange={this.toggleCreatePinForDestinationCheckbox} />} />
+ </div>
+ )}
+ </>
+ )}
+ <button id="get-routes-button" disabled={this.selectedDestinationFeature ? false : true} onClick={() => this.getRoutes(this.selectedDestinationFeature)}>
+ Get routes
+ </button>
+
+ {/* <input
+ placeholder="Origin"
+ /> */}
+ </div>
+ )}
+ {this.currentRouteInfoMap && (
+ <div className="current-route-info-container">
+ <div className="transportation-icons-container">
+ <IconButton
+ tooltip="Driving route"
+ onPointerDown={() => this.handleTransportationTypeChange('driving')}
+ icon={<FontAwesomeIcon icon={faCar as IconLookup} />}
+ color={this.selectedTransportationType === 'driving' ? 'lightblue' : 'grey'}
+ />
+ <IconButton
+ tooltip="Cycling route"
+ onPointerDown={() => this.handleTransportationTypeChange('cycling')}
+ icon={<FontAwesomeIcon icon={faBicycle as IconLookup} />}
+ color={this.selectedTransportationType === 'cycling' ? 'lightblue' : 'grey'}
+ />
+ <IconButton
+ tooltip="Walking route"
+ onPointerDown={() => this.handleTransportationTypeChange('walking')}
+ icon={<FontAwesomeIcon icon={faPersonWalking as IconLookup} />}
+ color={this.selectedTransportationType === 'walking' ? 'lightblue' : 'grey'}
+ />
+ </div>
+ <div className="selected-route-details-container">
+ <div>Duration: {this.currentRouteInfoMap[this.selectedTransportationType].duration}</div>
+ <div>Distance: {this.currentRouteInfoMap[this.selectedTransportationType].distance}</div>
+ </div>
+ </div>
+ )}
+ {this.menuType === 'customize' && (
+ <div className="customized-marker-container">
+ <div className="current-marker-container">
+ <div>Current Marker: </div>
+ <div>{this.getMarkerIcon()}</div>
+ </div>
+ <div className="color-picker-container" style={{ marginBottom: '10px' }}>
+ <CirclePicker circleSize={15} circleSpacing={7} width="100%" onChange={color => this.onMarkerColorChange(color)} />
+ </div>
+ <div className="all-markers-container">
+ {Object.keys(MarkerIcons.FAMarkerIconsMap).map(iconKey => (
+ <div key={iconKey} className="marker-icon">
+ <IconButton onPointerDown={() => this.onMarkerIconChange(iconKey)} icon={MarkerIcons.getFontAwesomeIcon(iconKey, '1x', 'white')} />
+ </div>
+ ))}
+ </div>
+ <div style={{ width: '100%', height: '3px', color: 'white' }}></div>
+ </div>
+ )}
+ {this.menuType === 'route' && this.routeDoc && <div>{StrCast(this.routeDoc.title)}</div>}
{buttons}
- </div>
+ </div>,
+ true
);
}
}
diff --git a/src/client/views/nodes/MapBox/MapBox.scss b/src/client/views/nodes/MapBox/MapBox.scss
index 9b00c30cf..8353ecc0e 100644
--- a/src/client/views/nodes/MapBox/MapBox.scss
+++ b/src/client/views/nodes/MapBox/MapBox.scss
@@ -14,15 +14,124 @@
.mapBox-searchbar {
display: flex;
flex-direction: row;
+ gap: 5px;
+ align-items: center;
width: calc(100% - 40px);
- .editableText-container {
- width: 100%;
- font-size: 16px !important;
+
+ // .editableText-container {
+ // width: 100%;
+ // font-size: 16px !important;
+ // }
+ // input {
+ // width: 100%;
+ // }
+ }
+
+ .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;
}
- input {
+
+ .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;
+ flex-direction: column;
+ justify-content: center;
+ align-items: flex-start;
+ position: absolute;
+ 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%;
+ padding: 10px;
+ &:hover{
+ background-color: lighten(rgb(187, 187, 187), 10%);
+ }
}
+
+ }
+
+ .animation-panel {
+ z-index: 900;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: flex-start;
+ position: absolute;
+ background-color: rgb(187, 187, 187);
+ padding: 10px;
+ border-top-right-radius: 5px;
+ border-bottom-right-radius: 5px;
+ width: 100%;
+
+ #route-to-animate-title {
+ font-size: 1.25em;
+ font-weight: bold;
+ }
+
+ .route-animation-options {
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ gap: 7px;
+
+ .animation-suboptions{
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ gap: 7px;
+
+ label{
+ margin-bottom: 0;
+ }
+
+ .speed-label{
+ margin-right: 5px;
+ }
+
+ #last-divider{
+ margin-left: 10px;
+ margin-right: 10px;
+ }
+ }
+
+
+ }
+
}
+
+
.mapBox-topbar {
display: flex;
flex-direction: row;
@@ -106,3 +215,4 @@
display: block;
}
}
+
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index a420e0101..8b5858e28 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -1,15 +1,18 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import BingMapsReact from 'bingmaps-react';
-import { Button, EditableText, IconButton, Type } from 'browndash-components';
-import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
+// import 'mapbox-gl/dist/mapbox-gl.css';
+
+import { Button, EditableText, IconButton, Size, Type } from 'browndash-components';
+import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, makeObservable, flow, toJS } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Utils, emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, setupMoveUpEvents } from '../../../../Utils';
import { Doc, DocListCast, Field, LinkedTo, Opt } from '../../../../fields/Doc';
import { DocCss, Highlight } from '../../../../fields/DocSymbols';
-import { DocCast, NumCast, StrCast } from '../../../../fields/Types';
+import { Id } from '../../../../fields/FieldSymbols';
+import { Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
+import { Docs, DocUtils } from '../../../documents/Documents';
import { DocumentType } from '../../../documents/DocumentTypes';
-import { DocUtils, Docs } from '../../../documents/Documents';
import { DocumentManager } from '../../../util/DocumentManager';
import { DragManager } from '../../../util/DragManager';
import { LinkManager } from '../../../util/LinkManager';
@@ -24,7 +27,43 @@ 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 './MapBox.scss';
+import { NumberLiteralType } from 'typescript';
+// import { GeocoderControl } from './GeocoderControl';
+import mapboxgl, { LngLat, LngLatBoundsLike, MapLayerMouseEvent, MercatorCoordinate } from 'mapbox-gl!';
+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, Checkbox, FormControlLabel, TextField } from '@mui/material';
+import { List } from '../../../../fields/List';
+import { listSpec } from '../../../../fields/Schema';
+import { IconLookup, faCircleXmark, faFileExport, faGear, faPause, faPlay, faRotate } from '@fortawesome/free-solid-svg-icons';
+import { MarkerIcons } from './MarkerIcons';
+import { SettingsManager } from '../../../util/SettingsManager';
+import * as turf from '@turf/turf';
+import * as d3 from 'd3';
+import { AnimationSpeed, AnimationStatus, AnimationUtility } from './AnimationUtility';
+import { fastSpeedIcon, mediumSpeedIcon, slowSpeedIcon } from './AnimationSpeedIcons';
+
// amongus
/**
* MapBox architecture:
@@ -40,6 +79,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
@@ -65,6 +128,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 } = {};
constructor(props: any) {
@@ -85,6 +149,94 @@ 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 updatedRouteCoordinates(): Feature<Geometry, GeoJsonProperties> {
+ if (this.routeToAnimate?.routeCoordinates) {
+ const originalCoordinates: Position[] = JSON.parse(StrCast(this.routeToAnimate.routeCoordinates));
+ // const index = Math.floor(this.animationPhase * originalCoordinates.length);
+ const index = this.animationPhase * (originalCoordinates.length - 1); // Calculate the fractional index
+ const startIndex = Math.floor(index);
+ const endIndex = Math.ceil(index);
+
+ if (startIndex === endIndex) {
+ // AnimationPhase is at a whole number (no interpolation needed)
+ const coordinates = [originalCoordinates[startIndex]];
+ const geometry: LineString = {
+ type: 'LineString',
+ coordinates,
+ };
+ return {
+ type: 'Feature',
+ properties: {
+ routeTitle: StrCast(this.routeToAnimate.title),
+ },
+ geometry: geometry,
+ };
+ } else {
+ // Interpolate between two coordinates
+ const startCoord = originalCoordinates[startIndex];
+ const endCoord = originalCoordinates[endIndex];
+ const fraction = index - startIndex;
+
+ // Interpolate the coordinates
+ const interpolatedCoord = [startCoord[0] + fraction * (endCoord[0] - startCoord[0]), startCoord[1] + fraction * (endCoord[1] - startCoord[1])];
+
+ const coordinates = originalCoordinates.slice(0, startIndex + 1).concat([interpolatedCoord]);
+
+ const geometry: LineString = {
+ type: 'LineString',
+ coordinates,
+ };
+ return {
+ type: 'Feature',
+ properties: {
+ routeTitle: StrCast(this.routeToAnimate.title),
+ },
+ geometry: geometry,
+ };
+ }
+ }
+ return {
+ type: 'Feature',
+ properties: {},
+ geometry: {
+ type: 'LineString',
+ coordinates: [],
+ },
+ };
+ }
+ @computed get selectedRouteCoordinates(): Position[] {
+ let coordinates: Position[] = [];
+ if (this.routeToAnimate?.routeCoordinates) {
+ coordinates = JSON.parse(StrCast(this.routeToAnimate.routeCoordinates));
+ }
+ return coordinates;
+ }
+
+ @computed get allRoutesGeoJson(): FeatureCollection {
+ const features: Feature<Geometry, GeoJsonProperties>[] = this.allRoutes.map((routeDoc: Doc) => {
+ console.log('Route coords: ', routeDoc.routeCoordinates);
+ const geometry: LineString = {
+ type: 'LineString',
+ coordinates: JSON.parse(StrCast(routeDoc.routeCoordinates)),
+ };
+ return {
+ type: 'Feature',
+ properties: {
+ routeTitle: routeDoc.title,
+ },
+ geometry: geometry,
+ };
+ });
+
+ return {
+ type: 'FeatureCollection',
+ features: features,
+ };
+ }
+
@computed get SidebarShown() {
return this.layoutDoc._layout_showSidebar ? true : false;
}
@@ -105,7 +257,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
componentWillUnmount(): void {
this._unmounting = true;
- this.deselectPin();
+ this.deselectPinOrRoute();
this._rerenderTimeout && clearTimeout(this._rerenderTimeout);
Object.keys(this._disposers).forEach(key => this._disposers[key]?.());
}
@@ -120,7 +272,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
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;
+ let existingPin = this.allPushpins.find(pin => pin.latitude === doc.latitude && pin.longitude === doc.longitude) ?? this.selectedPinOrRoute;
if (doc.latitude !== undefined && doc.longitude !== undefined && !existingPin) {
existingPin = this.createPushpin(NumCast(doc.latitude), NumCast(doc.longitude), StrCast(doc.map));
}
@@ -221,10 +373,10 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
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;
+ if (note && this.selectedPinOrRoute) {
+ note.latitude = this.selectedPinOrRoute.latitude;
+ note.longitude = this.selectedPinOrRoute.longitude;
+ note.map = this.selectedPinOrRoute.map;
}
return note as Doc;
});
@@ -250,10 +402,10 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
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;
+ if (note && this.selectedPinOrRoute) {
+ note.latitude = this.selectedPinOrRoute.latitude;
+ note.longitude = this.selectedPinOrRoute.longitude;
+ note.map = this.selectedPinOrRoute.map;
}
}),
'create note annotation'
@@ -328,49 +480,28 @@ 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 = undefined;
+ @observable selectedPinOrRoute: Doc | undefined;
@action
- deselectPin = () => {
- if (this.selectedPin) {
- // Removes filter
- Doc.setDocFilter(this.layoutDoc, 'latitude', this.selectedPin.latitude, 'remove');
- Doc.setDocFilter(this.layoutDoc, 'longitude', this.selectedPin.longitude, 'remove');
- Doc.setDocFilter(this.layoutDoc, 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.dataDoc.map;
+ deselectPinOrRoute = () => {
+ if (this.selectedPinOrRoute) {
+ // // Removes filter
+ // Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'remove');
+ // Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'remove');
+ // Doc.setDocFilter(this.Document, 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.Document.map;
}
};
@@ -383,22 +514,22 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
*/
@action
pushpinClicked = (pinDoc: Doc) => {
- this.deselectPin();
- this.selectedPin = pinDoc;
+ this.deselectPinOrRoute();
+ this.selectedPinOrRoute = pinDoc;
this.bingSearchBarContents = pinDoc.map;
// Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'match');
// Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'match');
- Doc.setDocFilter(this.layoutDoc, LinkedTo, `mapPin=${Field.toScriptString(this.selectedPin)}`, 'check');
+ Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(this.selectedPinOrRoute)}`, 'check');
- this.recolorPin(this.selectedPin, 'green');
+ this.recolorPin(this.selectedPinOrRoute, 'green');
- MapAnchorMenu.Instance.Delete = this.deleteSelectedPin;
+ MapAnchorMenu.Instance.Delete = this.deleteSelectedPinOrRoute;
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 point = this._bingMap.current.tryLocationToPixel(new this.MicrosoftMaps.Location(this.selectedPinOrRoute.latitude, this.selectedPinOrRoute.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);
@@ -413,7 +544,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@action
mapOnClick = (e: { location: { latitude: any; longitude: any } }) => {
this._props.select(false);
- this.deselectPin();
+ this.deselectPinOrRoute();
};
/*
* Updates values of layout doc to match the current map
@@ -460,14 +591,14 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
/// this should use SELECTED pushpin for lat/long if there is a selection, otherwise CENTER
const anchor = Docs.Create.ConfigDocument({
title: 'MapAnchor:' + this.Document.title,
- text: StrCast(this.selectedPin?.map) || StrCast(this.dataDoc.map) || 'map location',
- config_latitude: NumCast((existingPin ?? this.selectedPin)?.latitude ?? this.dataDoc.latitude),
- config_longitude: NumCast((existingPin ?? this.selectedPin)?.longitude ?? this.dataDoc.longitude),
+ text: StrCast(this.selectedPinOrRoute?.map) || StrCast(this.Document.map) || 'map location',
+ config_latitude: NumCast((existingPin ?? this.selectedPinOrRoute)?.latitude ?? this.dataDoc.latitude),
+ config_longitude: NumCast((existingPin ?? this.selectedPinOrRoute)?.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),
+ config_map: StrCast((existingPin ?? this.selectedPinOrRoute)?.map) || StrCast(this.dataDoc.map),
layout_unrendered: true,
- mapPin: existingPin ?? this.selectedPin,
+ mapPin: existingPin ?? this.selectedPinOrRoute,
annotationOn: this.Document,
});
if (anchor) {
@@ -506,7 +637,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
* Removes pin from annotations
*/
@action
- removePushpin = (pinDoc: Doc) => this.removeMapDocument(pinDoc, this.annotationKey);
+ removePushpinOrRoute = (pinOrRouteDoc: Doc) => this.removeMapDocument(pinOrRouteDoc, this.annotationKey);
/*
* Removes pushpin from map render
@@ -516,18 +647,18 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this._bingMap.current.entities.remove(this.map_docToPinMap.get(pinDoc));
}
this.map_docToPinMap.delete(pinDoc);
- this.selectedPin = undefined;
+ this.selectedPinOrRoute = undefined;
};
@action
- deleteSelectedPin = undoable(() => {
- if (this.selectedPin) {
+ deleteSelectedPinOrRoute = undoable(() => {
+ if (this.selectedPinOrRoute) {
// Removes filter
- Doc.setDocFilter(this.layoutDoc, 'latitude', this.selectedPin.latitude, 'remove');
- Doc.setDocFilter(this.layoutDoc, 'longitude', this.selectedPin.longitude, 'remove');
- Doc.setDocFilter(this.layoutDoc, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove');
+ Doc.setDocFilter(this.Document, 'latitude', this.selectedPinOrRoute.latitude, 'remove');
+ Doc.setDocFilter(this.Document, 'longitude', this.selectedPinOrRoute.longitude, 'remove');
+ Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPinOrRoute))}`, 'remove');
- this.removePushpin(this.selectedPin);
+ this.removePushpinOrRoute(this.selectedPinOrRoute);
}
MapAnchorMenu.Instance.fadeOut(true);
document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
@@ -536,6 +667,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;
}
@@ -547,12 +679,17 @@ 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;
+ if (this.selectedPinOrRoute) {
+ this._mapRef.current?.flyTo({
+ center: [NumCast(this.selectedPinOrRoute.longitude), NumCast(this.selectedPinOrRoute.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);
};
@@ -584,16 +721,13 @@ 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);
};
/*
@@ -664,24 +798,26 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
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 => {
+ // 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) {
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;
@@ -698,8 +834,623 @@ 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=${NumCast(latitude)},lng=${NumCast(longitude)}`,
+ map: location,
+ description: '',
+ wikiData: wikiData,
+ markerType: 'MAP_PIN',
+ markerColor: '#ff5722',
+ }
+ // { 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: Position[], origin: string, destination: any, createPinForDestination: boolean) => {
+ const mapRoute = Docs.Create.MapRouteDocument(false, [], { title: `${origin} --> ${destination.place_name}`, routeCoordinates: JSON.stringify(coordinates) });
+ this.addDocument(mapRoute, this.annotationKey);
+ if (createPinForDestination) {
+ this.createPushpin(destination.center[1], destination.center[0], destination.place_name);
+ }
+ return mapRoute;
+
+ // mapMarker.infoWindowOpen = true;
+ }, 'createmaproute');
+
searchbarKeyDown = (e: any) => e.key === 'Enter' && this.bingSearch();
+ @observable
+ featuresFromGeocodeResults: any[] = [];
+
+ @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);
+
+ if (this._mapRef.current) {
+ this._mapRef.current.flyTo({
+ center: feature.center,
+ });
+ }
+ 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 && !this.isAnimating) {
+ runInAction(() => {
+ this.settingsOpen = false;
+ this.featuresFromGeocodeResults = features;
+ this.routeToAnimate = undefined;
+ });
+ }
+ // 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), []);
+
+ @action
+ handleMapClick = (e: MapLayerMouseEvent) => {
+ if (this._mapRef.current) {
+ const features = this._mapRef.current.queryRenderedFeatures(e.point, {
+ layers: ['map-routes-layer'],
+ });
+
+ console.error(features);
+ if (features && features.length > 0 && features[0].properties && features[0].geometry) {
+ const geometry = features[0].geometry as LineString;
+ const routeTitle: string = features[0].properties['routeTitle'];
+ const routeDoc: Doc | undefined = this.allRoutes.find(routeDoc => routeDoc.title === routeTitle);
+ this.deselectPinOrRoute(); // TODO: Also deselect route if selected
+ if (routeDoc) {
+ this.selectedPinOrRoute = routeDoc;
+ Doc.setDocFilter(this.Document, LinkedTo, `mapRoute=${Field.toScriptString(this.selectedPinOrRoute)}`, 'check');
+
+ // TODO: Recolor route
+
+ MapAnchorMenu.Instance.Delete = this.deleteSelectedPinOrRoute;
+ MapAnchorMenu.Instance.Center = this.centerOnSelectedPin;
+ MapAnchorMenu.Instance.OnClick = this.createNoteAnnotation;
+ MapAnchorMenu.Instance.StartDrag = this.startAnchorDrag;
+
+ MapAnchorMenu.Instance.setRouteDoc(routeDoc);
+
+ // TODO: Subject to change
+ 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;
+ MapAnchorMenu.Instance.OpenAnimationPanel = this.openAnimationPanel;
+
+ // this.selectedRouteCoordinates = geometry.coordinates;
+
+ MapAnchorMenu.Instance.setMenuType('route');
+
+ MapAnchorMenu.Instance.jumpTo(e.originalEvent.clientX, e.originalEvent.clientY, true);
+
+ document.addEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
+ }
+ }
+ }
+ };
+
+ /**
+ * 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
+ */
+ handleMapDblClick = 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.deselectPinOrRoute(); // TODO: check this method
+ this.selectedPinOrRoute = pinDoc;
+ // this.bingSearchBarContents = pinDoc.map;
+
+ // Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'match');
+ // Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'match');
+ Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(this.selectedPinOrRoute)}`, 'check');
+
+ this.recolorPin(this.selectedPinOrRoute, 'green'); // TODO: check this method
+
+ MapAnchorMenu.Instance.Delete = this.deleteSelectedPinOrRoute;
+ 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;
+
+ MapAnchorMenu.Instance.setMenuType('standard');
+
+ 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;
+ }
+ };
+
+ @observable
+ isAnimating: boolean = false;
+
+ @observable
+ isPaused: boolean = false;
+
+ @observable
+ routeToAnimate: Doc | undefined = undefined;
+
+ @observable
+ animationPhase: number = 0;
+
+ @observable
+ finishedFlyTo: boolean = false;
+
+ @action
+ setAnimationPhase = (newValue: number) => {
+ this.animationPhase = newValue;
+ };
+
+ @observable
+ frameId: number | null = null;
+
+ @action
+ setFrameId = (frameId: number) => {
+ this.frameId = frameId;
+ };
+
+ @action
+ openAnimationPanel = (routeDoc: Doc | undefined) => {
+ if (routeDoc) {
+ MapAnchorMenu.Instance.fadeOut(true);
+ document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
+ this.routeToAnimate = routeDoc;
+ }
+ };
+
+ @observable
+ animationDuration = 40000;
+
+ @observable
+ animationAltitude = 12800;
+
+ @observable
+ pathDistance = 0;
+
+ @observable
+ isStreetViewAnimation: boolean = false;
+
+ @observable
+ animationSpeed: AnimationSpeed = AnimationSpeed.MEDIUM;
+
+ @action
+ updateAnimationSpeed = () => {
+ switch (this.animationSpeed) {
+ case AnimationSpeed.SLOW:
+ this.animationSpeed = AnimationSpeed.MEDIUM;
+ break;
+ case AnimationSpeed.MEDIUM:
+ this.animationSpeed = AnimationSpeed.FAST;
+ break;
+ case AnimationSpeed.FAST:
+ this.animationSpeed = AnimationSpeed.SLOW;
+ break;
+ default:
+ this.animationSpeed = AnimationSpeed.MEDIUM;
+ break;
+ }
+ };
+ @computed get animationSpeedTooltipText(): string {
+ switch (this.animationSpeed) {
+ case AnimationSpeed.SLOW:
+ return '1x speed';
+ case AnimationSpeed.MEDIUM:
+ return '2x speed';
+ case AnimationSpeed.FAST:
+ return '3x speed';
+ default:
+ return '2x speed';
+ }
+ }
+ @computed get animationSpeedIcon(): JSX.Element {
+ switch (this.animationSpeed) {
+ case AnimationSpeed.SLOW:
+ return slowSpeedIcon;
+ case AnimationSpeed.MEDIUM:
+ return mediumSpeedIcon;
+ case AnimationSpeed.FAST:
+ return fastSpeedIcon;
+ default:
+ return mediumSpeedIcon;
+ }
+ }
+
+ @action
+ toggleIsStreetViewAnimation = () => {
+ this.isStreetViewAnimation = !this.isStreetViewAnimation;
+ };
+
+ @observable
+ dynamicRouteFeature: Feature<Geometry, GeoJsonProperties> = {
+ type: 'Feature',
+ properties: {},
+ geometry: {
+ type: 'LineString',
+ coordinates: [],
+ },
+ };
+
+ @observable
+ path: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties> = {
+ type: 'Feature',
+ geometry: {
+ type: 'LineString',
+ coordinates: [],
+ },
+ properties: {},
+ };
+
+ getFeatureFromRouteDoc = (routeDoc: Doc): Feature<Geometry, GeoJsonProperties> => {
+ const geometry: LineString = {
+ type: 'LineString',
+ coordinates: JSON.parse(StrCast(routeDoc.routeCoordinates)),
+ };
+ return {
+ type: 'Feature',
+ properties: {
+ routeTitle: routeDoc.title,
+ },
+ geometry: geometry,
+ };
+ };
+
+ @action
+ playAnimation = (status: AnimationStatus) => {
+ if (!this._mapRef.current || !this.routeToAnimate) {
+ return;
+ }
+
+ if (this.isAnimating) {
+ return;
+ }
+ this.animationPhase = status === AnimationStatus.RESUME ? this.animationPhase : 0;
+ this.frameId = AnimationStatus.RESUME ? this.frameId : null;
+ this.finishedFlyTo = AnimationStatus.RESUME ? this.finishedFlyTo : false;
+
+ const path = turf.lineString(this.selectedRouteCoordinates);
+
+ this.settingsOpen = false;
+ this.path = path;
+ this.pathDistance = turf.lineDistance(path);
+ this.isAnimating = true;
+ runInAction(() => {
+ return new Promise<void>(async resolve => {
+ let animationUtil;
+ try {
+ const targetLngLat = {
+ lng: this.selectedRouteCoordinates[0][0],
+ lat: this.selectedRouteCoordinates[0][1],
+ };
+
+ animationUtil = new AnimationUtility(targetLngLat, this.selectedRouteCoordinates, this.isStreetViewAnimation, this.animationSpeed);
+
+ const updateFrameId = (newFrameId: number) => {
+ this.setFrameId(newFrameId);
+ };
+
+ const updateAnimationPhase = (newAnimationPhase: number) => {
+ this.setAnimationPhase(newAnimationPhase);
+ };
+
+ if (status !== AnimationStatus.RESUME) {
+ const result = await animationUtil.flyInAndRotate({
+ map: this._mapRef.current!,
+ // targetLngLat,
+ // duration 3000
+ // startAltitude: 3000000,
+ // endAltitude: this.isStreetViewAnimation ? 80 : 12000,
+ // startBearing: 0,
+ // endBearing: -20,
+ // startPitch: 40,
+ // endPitch: this.isStreetViewAnimation ? 80 : 50,
+ updateFrameId,
+ });
+
+ console.log('Bearing: ', result.bearing);
+ console.log('Altitude: ', result.altitude);
+ }
+
+ runInAction(() => {
+ this.finishedFlyTo = true;
+ });
+
+ // follow the path while slowly rotating the camera, passing in the camera bearing and altitude from the previous animation
+ await animationUtil.animatePath({
+ map: this._mapRef.current!,
+ // path: this.path,
+ // startBearing: -20,
+ // startAltitude: this.isStreetViewAnimation ? 80 : 12000,
+ // pitch: this.isStreetViewAnimation ? 80: 50,
+ currentAnimationPhase: this.animationPhase,
+ updateAnimationPhase,
+ updateFrameId,
+ });
+
+ // get the bounds of the linestring, use fitBounds() to animate to a final view
+ const bbox3d = turf.bbox(this.path);
+
+ const bbox2d: LngLatBoundsLike = [bbox3d[0], bbox3d[1], bbox3d[2], bbox3d[3]];
+
+ this._mapRef.current!.fitBounds(bbox2d, {
+ duration: 3000,
+ pitch: 30,
+ bearing: 0,
+ padding: 120,
+ });
+
+ setTimeout(() => {
+ resolve();
+ }, 10000);
+ } catch (error: any) {
+ console.log(error);
+ console.log('animation util: ', animationUtil);
+ }
+ });
+ });
+ };
+
+ @action
+ pauseAnimation = () => {
+ if (this.frameId && this.animationPhase > 0) {
+ window.cancelAnimationFrame(this.frameId);
+ this.frameId = null;
+ this.isAnimating = false;
+ }
+ };
+
+ @action
+ stopAndCloseAnimation = () => {
+ if (this.frameId) {
+ window.cancelAnimationFrame(this.frameId);
+ this.frameId = null;
+ this.finishedFlyTo = false;
+ this.isAnimating = false;
+ this.animationPhase = 0;
+ this.routeToAnimate = undefined;
+ // this.selectedRouteCoordinates = [];
+ }
+ // reset bearing and pitch to original, zoom out
+ };
+
+ @action
+ exportAnimationToVideo = () => {};
+
+ getRouteAnimationOptions = (): JSX.Element => {
+ return (
+ <>
+ <IconButton
+ tooltip={this.isAnimating && this.finishedFlyTo ? 'Pause Animation' : 'Play Animation'}
+ onPointerDown={() => {
+ if (this.isAnimating && this.finishedFlyTo) {
+ this.pauseAnimation();
+ } else if (this.animationPhase > 0) {
+ this.playAnimation(AnimationStatus.RESUME); // Resume from the current phase
+ } else {
+ this.playAnimation(AnimationStatus.START); // Play from the beginning
+ }
+ }}
+ icon={this.isAnimating && this.finishedFlyTo ? <FontAwesomeIcon icon={faPause as IconLookup} /> : <FontAwesomeIcon icon={faPlay as IconLookup} />}
+ color="black"
+ size={Size.MEDIUM}
+ />
+ {this.isAnimating && this.finishedFlyTo && (
+ <IconButton tooltip="Restart animation" onPointerDown={() => this.playAnimation(AnimationStatus.RESTART)} icon={<FontAwesomeIcon icon={faRotate as IconLookup} />} color="black" size={Size.MEDIUM} />
+ )}
+ <IconButton tooltip="Stop and close animation" onPointerDown={this.stopAndCloseAnimation} icon={<FontAwesomeIcon icon={faCircleXmark as IconLookup} />} color="black" size={Size.MEDIUM} />
+ <IconButton style={{ marginRight: '10px' }} tooltip="Export to video" onPointerDown={this.exportAnimationToVideo} icon={<FontAwesomeIcon icon={faFileExport as IconLookup} />} color="black" size={Size.MEDIUM} />
+ {!this.isAnimating && (
+ <>
+ <div className="animation-suboptions">
+ <div>|</div>
+ <FormControlLabel label="Street view animation" labelPlacement="start" control={<Checkbox color="success" checked={this.isStreetViewAnimation} onChange={this.toggleIsStreetViewAnimation} />} />
+ <div id="last-divider">|</div>
+ <IconButton tooltip={this.animationSpeedTooltipText} onPointerDown={this.updateAnimationSpeed} icon={this.animationSpeedIcon} size={Size.MEDIUM} />
+ </div>
+ </>
+ )}
+ </>
+ );
+ };
+
+ @action
+ hideRoute = () => {
+ this.temporaryRouteSource = {
+ type: 'FeatureCollection',
+ features: [],
+ };
+ };
+
+ @observable
+ mapboxMapViewState: ViewState = {
+ zoom: 9,
+ longitude: -71.45,
+ latitude: 41.82,
+ pitch: 0,
+ bearing: 0,
+ padding: {
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0,
+ },
+ };
+
+ @observable
+ settingsOpen: boolean = false;
+
+ @observable
+ mapStyle: string = 'mapbox://styles/mapbox/streets-v12';
+
+ @observable
+ showTerrain: boolean = false;
+
+ @action
+ toggleSettings = () => {
+ if (!this.isAnimating && this.animationPhase == 0) {
+ this.featuresFromGeocodeResults = [];
+ this.settingsOpen = !this.settingsOpen;
+ }
+ };
+
+ @action
+ changeMapStyle = (e: React.ChangeEvent<HTMLSelectElement>) => {
+ 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<HTMLInputElement>) => {
+ 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<HTMLInputElement>) => {
+ const newVal = parseInt(e.target.value);
+ if (!isNaN(newVal) && newVal >= 0) {
+ this.mapboxMapViewState = {
+ ...this.mapboxMapViewState,
+ pitch: parseInt(e.target.value),
+ };
+ }
+ };
+
+ getMarkerIcon = (pinDoc: Doc): JSX.Element | null => {
+ const markerType = StrCast(pinDoc.markerType);
+ const markerColor = StrCast(pinDoc.markerColor);
+
+ return MarkerIcons.getFontAwesomeIcon(markerType, '2x', markerColor) ?? null;
+ };
+
static _firstRender = true;
static _rerenderDelay = 500;
_rerenderTimeout: any;
@@ -720,6 +1471,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return null;
}
+ const scale = this.props.NativeDimScaling?.() || 1;
return (
<div className="mapBox" ref={this._ref}>
<div
@@ -728,40 +1480,176 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
onPointerDown={async e => {
e.button === 0 && !e.ctrlKey && e.stopPropagation();
}}
- style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}>
- <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'} />} />
+ style={{ transformOrigin: 'top left', transform: `scale(${scale})`, width: `${100 / scale}%`, height: `${100 / scale}%`, pointerEvents: this.pointerEvents() }}>
+ {!this.routeToAnimate && (
+ <div className="mapBox-searchbar">
+ <TextField fullWidth placeholder="Enter a location" onChange={(e: any) => this.handleSearchChange(e.target.value)} />
+ <IconButton icon={<FontAwesomeIcon icon={faGear as IconLookup} size="1x" />} type={Type.TERT} onClick={e => this.toggleSettings()} />
+ </div>
+ )}
+ {this.settingsOpen && !this.routeToAnimate && (
+ <div className="mapbox-settings-panel" style={{ right: `${0 + this.sidebarWidth()}px` }}>
+ <div className="mapbox-style-select">
+ <div>Map Style:</div>
+ <div>
+ <select onChange={this.changeMapStyle}>
+ <option value="streets-v12">Streets</option>
+ <option value="outdoors-v12">Outdoors</option>
+ <option value="light-v11">Light</option>
+ <option value="dark-v11">Dark</option>
+ <option value="satellite-v9">Satellite</option>
+ <option value="satellite-streets-v12">Satellite Streets</option>
+ <option value="navigation-day-v1">Navigation Day</option>
+ <option value="navigation-night-v1">Navigation Night</option>
+ </select>
+ </div>
+ </div>
+ <div className="mapbox-bearing-selection">
+ <div>Bearing: </div>
+ <input value={this.mapboxMapViewState.bearing} min={0} type="number" onChange={this.onBearingChange} />
+ </div>
+ <div className="mapbox-pitch-selection">
+ <div>Pitch: </div>
+ <input value={this.mapboxMapViewState.pitch} min={0} type="number" onChange={this.onPitchChange} />
+ </div>
+ <div className="mapbox-terrain-selection">
+ <div>Show terrain: </div>
+ <input type="checkbox" checked={this.showTerrain} onChange={this.toggleShowTerrain} />
+ </div>
+ </div>
+ )}
+ {this.routeToAnimate && (
+ <div className="animation-panel">
+ <div id="route-to-animate-title">{StrCast(this.routeToAnimate.title)}</div>
+ <div className="route-animation-options">{this.getRouteAnimationOptions()}</div>
</div>
- </div>
+ )}
+ {this.featuresFromGeocodeResults.length > 0 && (
+ <div className="mapbox-geocoding-search-results">
+ <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}
+ mapboxAccessToken={MAPBOX_ACCESS_TOKEN}
+ id="mapbox-map"
+ mapStyle={this.mapStyle}
+ style={{ height: '100%', width: '100%' }}
+ initialViewState={this.isAnimating ? undefined : this.mapboxMapViewState}
+ // {...this.mapboxMapViewState}
+ onMove={this.onMapMove}
+ onClick={this.handleMapClick}
+ onDblClick={this.handleMapDblClick}
+ terrain={this.showTerrain ? { source: 'mapbox-dem', exaggeration: 2.0 } : undefined}>
+ <Source id="mapbox-dem" type="raster-dem" url="mapbox://mapbox.mapbox-terrain-dem-v1" tileSize={512} maxzoom={14} />
+ <Source id="temporary-route" type="geojson" data={this.temporaryRouteSource} />
+ <Source id="map-routes" type="geojson" data={this.allRoutesGeoJson} />
+ <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.isAnimating && this.animationPhase == 0 && <Layer id="map-routes-layer" type="line" source="map-routes" layout={{ 'line-join': 'round', 'line-cap': 'round' }} paint={{ 'line-color': '#FF0000', 'line-width': 4 }} />}
+ {this.routeToAnimate && (this.isAnimating || this.animationPhase > 0) && (
+ <>
+ {!this.isStreetViewAnimation && (
+ <>
+ <Source id="animated-route" type="geojson" data={this.updatedRouteCoordinates} />
+ <Layer
+ id="dynamic-animation-line"
+ type="line"
+ source="animated-route"
+ paint={{
+ 'line-color': 'yellow',
+ 'line-width': 4,
+ }}
+ />
+ </>
+ )}
+ <Source id="start-pin-base" type="geojson" data={AnimationUtility.createGeoJSONCircle(this.selectedRouteCoordinates[0], 0.04)} />
+ <Source id="start-pin-top" type="geojson" data={AnimationUtility.createGeoJSONCircle(this.selectedRouteCoordinates[0], 0.25)} />
+ <Source id="end-pin-base" type="geojson" data={AnimationUtility.createGeoJSONCircle(this.selectedRouteCoordinates.slice(-1)[0], 0.04)} />
+ <Source id="end-pin-top" type="geojson" data={AnimationUtility.createGeoJSONCircle(this.selectedRouteCoordinates.slice(-1)[0], 0.25)} />
+ <Layer
+ id="start-fill-pin-base"
+ type="fill-extrusion"
+ source="start-pin-base"
+ paint={{
+ 'fill-extrusion-color': '#0bfc03',
+ 'fill-extrusion-height': 1000,
+ }}
+ />
+ <Layer
+ id="start-fill-pin-top"
+ type="fill-extrusion"
+ source="start-pin-top"
+ paint={{
+ 'fill-extrusion-color': '#0bfc03',
+ 'fill-extrusion-base': 1000,
+ 'fill-extrusion-height': 1200,
+ }}
+ />
+ <Layer
+ id="end-fill-pin-base"
+ type="fill-extrusion"
+ source="end-pin-base"
+ paint={{
+ 'fill-extrusion-color': '#eb1c1c',
+ 'fill-extrusion-height': 1000,
+ }}
+ />
+ <Layer
+ id="end-fill-pin-top"
+ type="fill-extrusion"
+ source="end-pin-top"
+ paint={{
+ 'fill-extrusion-color': '#eb1c1c',
+ 'fill-extrusion-base': 1000,
+ 'fill-extrusion-height': 1200,
+ }}
+ />
+ </>
+ )}
+
+ <>
+ {!this.isAnimating &&
+ this.animationPhase == 0 &&
+ 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.getMarkerIcon(pushpin)}
+ </Marker>
+ ))}
+ </>
- <BingMapsReact
+ {/* {this.mapMarkers.length > 0 && this.mapMarkers.map((marker, idx) => (
+ <Marker key={idx} longitude={marker.longitude} latitude={marker.latitude}/>
+ ))} */}
+ </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
@@ -791,7 +1679,18 @@ 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}
+ 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}` }}>
@@ -817,3 +1716,54 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
);
}
}
+
+{
+ /* <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
+ 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> */
+}
diff --git a/src/client/views/nodes/MapBox/MapboxApiUtility.ts b/src/client/views/nodes/MapBox/MapboxApiUtility.ts
new file mode 100644
index 000000000..011b6f72a
--- /dev/null
+++ b/src/client/views/nodes/MapBox/MapboxApiUtility.ts
@@ -0,0 +1,105 @@
+
+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;
+
+ console.log(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/MapBox/MarkerIcons.tsx b/src/client/views/nodes/MapBox/MarkerIcons.tsx
new file mode 100644
index 000000000..146f296c1
--- /dev/null
+++ b/src/client/views/nodes/MapBox/MarkerIcons.tsx
@@ -0,0 +1,76 @@
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { faShopify } from '@fortawesome/free-brands-svg-icons';
+import { faBasketball, faBicycle, faBowlFood, faBus, faCameraRetro, faCar, faCartShopping, faFilm, faFootball, faFutbol, faHockeyPuck, faHospital, faHotel, faHouse, faLandmark, faLocationDot, faLocationPin, faMapPin, faMasksTheater, faMugSaucer, faPersonHiking, faPlane, faSchool, faShirt, faShop, faSquareParking, faStar, faTrainSubway, faTree, faUtensils, faVolleyball } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import React = require('react');
+
+export class MarkerIcons {
+
+ // static getMapboxIcon = (color: string) => {
+ // return (
+ // <svg xmlns="http://www.w3.org/2000/svg" id="marker" data-name="marker" width="20" height="48" viewBox="0 0 20 35">
+ // <g id="mapbox-marker-icon">
+ // <g id="icon">
+ // <ellipse id="shadow" cx="10" cy="27" rx="9" ry="5" fill="#c4c4c4" opacity="0.3" />
+ // <g id="mask" opacity="0.3">
+ // <g id="group">
+ // <path id="shadow-2" data-name="shadow" fill="#bfbfbf" d="M10,32c5,0,9-2.2,9-5s-4-5-9-5-9,2.2-9,5S5,32,10,32Z" fillRule="evenodd"/>
+ // </g>
+ // </g>
+ // <path id="color" fill={color} strokeWidth="0.5" d="M19.25,10.4a13.0663,13.0663,0,0,1-1.4607,5.2235,41.5281,41.5281,0,0,1-3.2459,5.5483c-1.1829,1.7369-2.3662,3.2784-3.2541,4.3859-.4438.5536-.8135.9984-1.0721,1.3046-.0844.1-.157.1852-.2164.2545-.06-.07-.1325-.1564-.2173-.2578-.2587-.3088-.6284-.7571-1.0723-1.3147-.8879-1.1154-2.0714-2.6664-3.2543-4.41a42.2677,42.2677,0,0,1-3.2463-5.5535A12.978,12.978,0,0,1,.75,10.4,9.4659,9.4659,0,0,1,10,.75,9.4659,9.4659,0,0,1,19.25,10.4Z"/>
+ // <path id="circle" fill="#fff" stroke='white' strokeWidth="0.5" d="M13.55,10A3.55,3.55,0,1,1,10,6.45,3.5484,3.5484,0,0,1,13.55,10Z"/>
+ // </g>
+ // </g>
+ // <rect width="20" height="48" fill="none"/>
+ // </svg>
+ // )
+ // }
+
+ static getFontAwesomeIcon(key: string, size: string, color?: string): JSX.Element {
+ const icon: IconProp = MarkerIcons.FAMarkerIconsMap[key];
+ const iconProps: any = { icon };
+
+ if (color) {
+ iconProps.color = color;
+ }
+
+ return (<FontAwesomeIcon {...iconProps} size={size} />);
+
+
+ }
+
+ static FAMarkerIconsMap: {[key: string]: IconProp} = {
+ 'MAP_PIN': faLocationDot,
+ 'RESTAURANT_ICON': faUtensils,
+ 'HOTEL_ICON': faHotel,
+ 'HOUSE_ICON': faHouse,
+ 'AIRPLANE_ICON': faPlane,
+ 'CAR_ICON': faCar,
+ 'BUS_ICON': faBus,
+ 'TRAIN_ICON': faTrainSubway,
+ 'BICYCLE_ICON': faBicycle,
+ 'PARKING_ICON': faSquareParking,
+ 'PHOTO_ICON': faCameraRetro,
+ 'CAFE_ICON': faMugSaucer,
+ 'STAR_ICON': faStar,
+ 'SHOPPING_CART_ICON': faCartShopping,
+ 'SHOPIFY_ICON': faShopify,
+ 'SHOP_ICON': faShop,
+ 'SHIRT_ICON': faShirt,
+ 'FOOD_ICON': faBowlFood,
+ 'LANDMARK_ICON': faLandmark,
+ 'HOSPITAL_ICON': faHospital,
+ 'NATURE_ICON': faTree,
+ 'HIKING_ICON': faPersonHiking,
+ 'SOCCER_ICON': faFutbol,
+ 'VOLLEYBALL_ICON': faVolleyball,
+ 'BASKETBALL_ICON': faBasketball,
+ 'HOCKEY_ICON': faHockeyPuck,
+ 'FOOTBALL_ICON': faFootball,
+ 'SCHOOL_ICON': faSchool,
+ 'THEATER_ICON': faMasksTheater,
+ 'FILM_ICON': faFilm
+ }
+
+
+} \ No newline at end of file
diff --git a/src/client/views/nodes/MapBox/icon_images/mapbox-marker-icon-20px-blue.png b/src/client/views/nodes/MapBox/icon_images/mapbox-marker-icon-20px-blue.png
new file mode 100644
index 000000000..8b686e2aa
--- /dev/null
+++ b/src/client/views/nodes/MapBox/icon_images/mapbox-marker-icon-20px-blue.png
Binary files differ
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>
+ );
+ }
+}