diff options
Diffstat (limited to 'src/client/views/nodes/MapBox/MapBox.tsx')
-rw-r--r-- | src/client/views/nodes/MapBox/MapBox.tsx | 776 |
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> ))} </> |