aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/MapBox/MapBox.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/MapBox/MapBox.tsx')
-rw-r--r--src/client/views/nodes/MapBox/MapBox.tsx776
1 files changed, 663 insertions, 113 deletions
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index ac926e1fb..cde68a2e6 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -2,7 +2,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import BingMapsReact from 'bingmaps-react';
// import 'mapbox-gl/dist/mapbox-gl.css';
-import { Button, EditableText, IconButton, Type } from 'browndash-components';
+import { Button, EditableText, IconButton, Size, Type } from 'browndash-components';
import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, flow, toJS} from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -51,14 +51,20 @@ import debounce from 'debounce';
import './MapBox.scss';
import { NumberLiteralType } from 'typescript';
// import { GeocoderControl } from './GeocoderControl';
-import mapboxgl, { LngLat, MapLayerMouseEvent } from 'mapbox-gl';
+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, faGear } from '@fortawesome/free-solid-svg-icons';
+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
/**
@@ -142,25 +148,93 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@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(route => {
- console.log("Route coords: ", route.coordinates);
+ 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(route.coordinates))
- }
+ coordinates: JSON.parse(StrCast(routeDoc.routeCoordinates)),
+ };
return {
- type: 'Feature',
- properties: {},
- geometry: geometry
+ type: 'Feature',
+ properties: {
+ 'routeTitle': routeDoc.title},
+ geometry: geometry,
};
- });
-
- return {
+ });
+
+ return {
type: 'FeatureCollection',
- features: features
- };
+ features: features,
+ };
}
+
@computed get SidebarShown() {
return this.layoutDoc._layout_showSidebar ? true : false;
}
@@ -184,7 +258,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
_unmounting = false;
componentWillUnmount(): void {
this._unmounting = true;
- this.deselectPin();
+ this.deselectPinOrRoute();
this._rerenderTimeout && clearTimeout(this._rerenderTimeout);
Object.keys(this._disposers).forEach(key => this._disposers[key]?.());
}
@@ -199,7 +273,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));
}
@@ -300,10 +374,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;
});
@@ -329,10 +403,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'
@@ -410,11 +484,12 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
};
// The pin that is selected
- @observable selectedPin: Doc | undefined;
+ @observable selectedPinOrRoute: Doc | undefined;
+
@action
- deselectPin = () => {
- if (this.selectedPin) {
+ deselectPinOrRoute = () => {
+ if (this.selectedPinOrRoute) {
// // Removes filter
// Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPin.latitude, 'remove');
// Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'remove');
@@ -435,6 +510,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
};
+
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)));
@@ -444,22 +520,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.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');
+ Doc.setDocFilter(this.rootDoc, 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);
@@ -474,7 +550,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
@@ -520,14 +596,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.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),
+ text: StrCast(this.selectedPinOrRoute?.map) || StrCast(this.rootDoc.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.rootDoc,
});
if (anchor) {
@@ -566,7 +642,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
@@ -576,23 +652,25 @@ 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.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');
+ Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPinOrRoute.latitude, 'remove');
+ Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPinOrRoute.longitude, 'remove');
+ Doc.setDocFilter(this.rootDoc, 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);
}, 'delete pin');
+
+
tryHideMapAnchorMenu = (e: PointerEvent) => {
let target = document.elementFromPoint(e.x, e.y);
while (target) {
@@ -608,9 +686,9 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@action
centerOnSelectedPin = () => {
- if (this.selectedPin) {
+ if (this.selectedPinOrRoute) {
this._mapRef.current?.flyTo({
- center: [NumCast(this.selectedPin.longitude), NumCast(this.selectedPin.latitude)]
+ center: [NumCast(this.selectedPinOrRoute.longitude), NumCast(this.selectedPinOrRoute.latitude)]
})
}
// if (this.selectedPin) {
@@ -776,12 +854,12 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
false,
[],
{
- title: location ?? `lat=${latitude},lng=${longitude}`,
+ title: location ?? `lat=${NumCast(latitude)},lng=${NumCast(longitude)}`,
map: location,
description: "",
wikiData: wikiData,
- markerType: 'MAPBOX_MARKER',
- markerColor: '#3FB1CE'
+ markerType: 'MAP_PIN',
+ markerColor: '#ff5722'
},
// { title: map ?? `lat=${latitude},lng=${longitude}`, map: map },
// ,'pushpinIDamongus'+ this.incrementer++
@@ -794,14 +872,16 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}, 'createpin');
@action
- createMapRoute = undoable((coordinates: Position[], origin: string, destination: string) => {
- console.log(coordinates);
+ createMapRoute = undoable((coordinates: Position[], origin: string, destination: any, createPinForDestination: boolean) => {
const mapRoute = Docs.Create.MapRouteDocument(
false,
[],
- {title: `${origin} -> ${destination}`, routeCoordinates: JSON.stringify(coordinates)},
+ {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;
@@ -853,10 +933,11 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
*/
handleSearchChange = async (searchText: string) => {
const features = await MapboxApiUtility.forwardGeocodeForFeatures(searchText);
- if (features){
+ if (features && !this.isAnimating){
runInAction(() => {
this.settingsOpen= false;
this.featuresFromGeocodeResults = features;
+ this.routeToAnimate = undefined;
})
}
// try {
@@ -874,12 +955,65 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
// @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.rootDoc, 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
*/
- handleMapClick = async (e: MapLayerMouseEvent) => {
+ handleMapDblClick = async (e: MapLayerMouseEvent) => {
e.preventDefault();
const lngLat: LngLat = e.lngLat;
const longitude: number = lngLat.lng;
@@ -914,18 +1048,18 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@action
handleMarkerClick = (e: MarkerEvent<mapboxgl.Marker, MouseEvent>, pinDoc: Doc) => {
this.featuresFromGeocodeResults = [];
- this.deselectPin(); // TODO: check this method
- this.selectedPin = pinDoc;
+ this.deselectPinOrRoute(); // TODO: check this method
+ this.selectedPinOrRoute = 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');
+ Doc.setDocFilter(this.rootDoc, LinkedTo, `mapPin=${Field.toScriptString(this.selectedPinOrRoute)}`, 'check');
- this.recolorPin(this.selectedPin, 'green'); // TODO: check this method
+ this.recolorPin(this.selectedPinOrRoute, 'green'); // TODO: check this method
- 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;
@@ -941,11 +1075,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
MapAnchorMenu.Instance.AddNewRouteToMap = this.createMapRoute;
MapAnchorMenu.Instance.CreatePin = this.addMarkerForFeature;
- // const longitude = NumCast(pinDoc.longitude);
- // const latitude = NumCast(pinDoc.longitude);
- // const x = longitude + (this.props.PanelWidth() - this.sidebarWidth()) / 2;
- // const y = latitude + this.props.PanelHeight() / 2 + 20;
- // const cpt = this.props.ScreenToLocalTransform().inverse().transformPoint(x, y);
+ MapAnchorMenu.Instance.setMenuType('standard');
+
MapAnchorMenu.Instance.jumpTo(e.originalEvent.clientX, e.originalEvent.clientY, true);
document.addEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
@@ -979,6 +1110,351 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
}
+ @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 = {
@@ -1015,8 +1491,10 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@action
toggleSettings = () => {
- this.featuresFromGeocodeResults = [];
- this.settingsOpen = !this.settingsOpen;
+ if (!this.isAnimating && this.animationPhase == 0) {
+ this.featuresFromGeocodeResults = [];
+ this.settingsOpen = !this.settingsOpen;
+ }
}
@action
@@ -1056,6 +1534,16 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
}
+ getMarkerIcon = (pinDoc: Doc): JSX.Element | null => {
+ const markerType = StrCast(pinDoc.markerType);
+ const markerColor = StrCast(pinDoc.markerColor);
+
+ return MarkerIcons.getFontAwesomeIcon(markerType, '2x', markerColor) ?? null;
+
+ }
+
+
+
@@ -1092,21 +1580,22 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
<div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>
{renderAnnotations(this.opaqueFilter)}
{SnappingManager.GetIsDragging() ? null : renderAnnotations()}
+ {!this.routeToAnimate &&
+ <div className="mapBox-searchbar">
+ <TextField
+ fullWidth
+ placeholder='Enter a location'
+ onChange={(e) => this.handleSearchChange(e.target.value)}
+ />
+ <IconButton
+ icon={<FontAwesomeIcon icon={faGear as IconLookup} size='1x'/>}
+ type={Type.TERT}
+ onClick={(e) => this.toggleSettings()}
- <div className="mapBox-searchbar">
- <TextField
- fullWidth
- placeholder='Enter a location'
- onChange={(e) => this.handleSearchChange(e.target.value)}
- />
- <IconButton
- icon={<FontAwesomeIcon icon={faGear as IconLookup} size='1x'/>}
- type={Type.TERT}
- onClick={(e) => this.toggleSettings()}
-
- />
- </div>
- {this.settingsOpen &&
+ />
+ </div>
+ }
+ {this.settingsOpen && !this.routeToAnimate &&
<div className='mapbox-settings-panel' style={{right: `${0+ this.sidebarWidth()}px`}}>
<div className='mapbox-style-select'>
<div>
@@ -1151,45 +1640,52 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
</div>
</div>
}
-
- <div className='mapbox-geocoding-search-results'>
- {this.featuresFromGeocodeResults.length > 0 && (
+ {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>
+ }
+ {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}
+ <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>
- </div>
))}
</React.Fragment>
- )}
- </div>
+
+ </div>
+ )}
<MapProvider>
<MapboxMap
ref={this._mapRef}
- initialViewState={{
- longitude: -100,
- latitude: 40,
- zoom: 3.5
- }}
mapboxAccessToken={MAPBOX_ACCESS_TOKEN}
id="mapbox-map"
mapStyle={this.mapStyle}
style={{height: '100%', width: '100%'}}
- {...this.mapboxMapViewState}
+ initialViewState={this.isAnimating ? undefined : this.mapboxMapViewState}
+ // {...this.mapboxMapViewState}
onMove={this.onMapMove}
- onDblClick={this.handleMapClick}
+ onClick={this.handleMapClick}
+ onDblClick={this.handleMapDblClick}
terrain={this.showTerrain ? { source: 'mapbox-dem', exaggeration: 2.0 } : undefined}
@@ -1210,6 +1706,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
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'
@@ -1217,9 +1714,60 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
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.allPushpins
+ {!this.isAnimating && this.animationPhase == 0 && this.allPushpins
// .filter(anno => !anno.layout_unrendered)
.map((pushpin, idx) => (
<Marker
@@ -1228,7 +1776,9 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
latitude={NumCast(pushpin.latitude)}
anchor='bottom'
onClick={(e: MarkerEvent<mapboxgl.Marker, MouseEvent>) => this.handleMarkerClick(e, pushpin)}
- />
+ >
+ {this.getMarkerIcon(pushpin)}
+ </Marker>
))}
</>