import { IconLookup, faCircleXmark, faGear, faPause, faPlay, faRotate } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Checkbox, FormControlLabel, TextField } from '@mui/material'; import * as turf from '@turf/turf'; import { IconButton, Size, Type } from 'browndash-components'; import * as d3 from 'd3'; import { Feature, FeatureCollection, GeoJsonProperties, Geometry, LineString, Position } from 'geojson'; import mapboxgl, { LngLat, LngLatBoundsLike, MapLayerMouseEvent } from 'mapbox-gl'; import { IReactionDisposer, ObservableMap, action, autorun, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { CirclePicker, ColorResult } from 'react-color'; import { Layer, MapProvider, MapRef, Map as MapboxMap, Marker, Source, ViewState, ViewStateChangeEvent } from 'react-map-gl'; import { MarkerEvent } from 'react-map-gl/dist/esm/types'; import { Utils, emptyFunction, 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 { 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'; import { SnappingManager } from '../../../util/SnappingManager'; import { UndoManager, undoable } from '../../../util/UndoManager'; import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent'; import { SidebarAnnos } from '../../SidebarAnnos'; import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm'; import { Colors } from '../../global/globalEnums'; import { DocumentView } from '../DocumentView'; import { FocusViewOptions, FieldView, FieldViewProps } from '../FieldView'; import { FormattedTextBox } from '../formattedText/FormattedTextBox'; import { PinProps, PresBox } from '../trails'; import { fastSpeedIcon, mediumSpeedIcon, slowSpeedIcon } from './AnimationSpeedIcons'; import { AnimationSpeed, AnimationStatus, AnimationUtility } from './AnimationUtility'; import { MapAnchorMenu } from './MapAnchorMenu'; import './MapBox.scss'; import { MapboxApiUtility, TransportationType } from './MapboxApiUtility'; import { MarkerIcons } from './MarkerIcons'; // import { GeocoderControl } from './GeocoderControl'; // 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 bingApiKey = process.env.BING_MAPS; // if you're running local, get a Bing Maps api key here: https://www.bingmapsportal.com/ and then add it to the .env file in the Dash-Web root directory as: _CLIENT_BING_MAPS= const MAPBOX_ACCESS_TOKEN = 'pk.eyJ1IjoiemF1bHRhdmFuZ2FyIiwiYSI6ImNscHgwNDd1MDA3MXIydm92ODdianp6cGYifQ.WFAqbhwxtMHOWSPtu0l2uQ'; const MAPBOX_FORWARD_GEOCODE_BASE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/'; const MAPBOX_REVERSE_GEOCODE_BASE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/'; type PopupInfo = { longitude: number; latitude: number; title: string; description: string; }; // export type GeocoderControlProps = Omit & { // mapboxAccessToken: string; // marker?: Omit; // position: ControlPosition; // onResult: (...args: any[]) => void; // }; type MapMarker = { longitude: number; latitude: number; }; /** * Consider integrating later: allows for drawing, circling, making shapes on map */ // 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 MapBox extends ViewBoxAnnotatableComponent() implements ViewBoxInterface { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(MapBox, fieldKey); } private _dragRef = React.createRef(); private _sidebarRef = React.createRef(); private _ref: React.RefObject = React.createRef(); private _mapRef: React.RefObject = React.createRef(); private _disposers: { [key: string]: IReactionDisposer } = {}; private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt) => void); constructor(props: FieldViewProps) { super(props); makeObservable(this); } @observable private _savedAnnotations = new ObservableMap(); @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 allRoutes() { return this.allAnnotations.filter(anno => anno.type === DocumentType.MAPROUTE); } @computed get updatedRouteCoordinates(): Feature { 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 console.log('Animation phase', this.animationPhase); const startIndex = Math.floor(index); const endIndex = Math.ceil(index); let feature: Feature; let geometry: LineString; if (startIndex === endIndex) { // AnimationPhase is at a whole number (no interpolation needed) const coordinates = [originalCoordinates[startIndex]]; geometry = { type: 'LineString', coordinates, }; feature = { 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; const interpolator = d3.interpolateArray(startCoord, endCoord); const interpolatedCoord = interpolator(fraction); const coordinates = originalCoordinates.slice(0, startIndex + 1).concat([interpolatedCoord]); geometry = { type: 'LineString', coordinates, }; feature = { type: 'Feature', properties: { routeTitle: StrCast(this.routeToAnimate.title), }, geometry: geometry, }; } autorun(() => { const animationUtil = this.animationUtility; const concattedCoordinates = geometry.coordinates.concat(originalCoordinates.slice(endIndex)); const newFeature: Feature = { type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: concattedCoordinates, }, }; if (animationUtil) { animationUtil.setPath(newFeature); } }); return feature; } console.log('ERROR'); 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[] = 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; } @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.setContentViewBox?.(this); } _unmounting = false; componentWillUnmount(): void { this._unmounting = true; this.deselectPinOrRoute(); 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.selectedPinOrRoute; 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 (
); } // 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.selectedPinOrRoute) { note.latitude = this.selectedPinOrRoute.latitude; note.longitude = this.selectedPinOrRoute.longitude; note.map = this.selectedPinOrRoute.map; } return note as Doc; }); const targetCreator = (annotationOn: Doc | undefined) => { const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); FormattedTextBox.SetSelectOnLoad(target); return target; }; const docView = this.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.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.selectedPinOrRoute) { note.latitude = this.selectedPinOrRoute.latitude; note.longitude = this.selectedPinOrRoute.longitude; note.map = this.selectedPinOrRoute.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) => 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.ScreenToLocalBoxXf().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.Document.map; // For Bing Maps: The contents of the Bing search bar (string) geoDataRequestOptions = { entityType: 'PopulatedPlace', }; // The pin that is selected @observable selectedPinOrRoute: Doc | undefined = undefined; @action 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; } }; getView = async (doc: Doc, options: FocusViewOptions) => { if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) { this.toggleSidebar(); options.didMove = true; } return new Promise>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); }; /* * Pushpin onclick */ @action pushpinClicked = (pinDoc: Doc) => { 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.Document, LinkedTo, `mapPin=${Field.toScriptString(this.selectedPinOrRoute)}`, 'check'); this.recolorPin(this.selectedPinOrRoute, 'green'); 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.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.ScreenToLocalBoxXf().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.deselectPinOrRoute(); }; /* * 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.Document.title, text: (StrCast(this.selectedPinOrRoute?.map) || StrCast(this.Document.map) || 'map location') as any, 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.selectedPinOrRoute)?.map) || StrCast(this.dataDoc.map), layout_unrendered: true, mapPin: existingPin ?? this.selectedPinOrRoute, annotationOn: this.Document, }); if (anchor) { if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; addAsAnnotation && this.addDocument(anchor); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), map: true } }, this.Document); return anchor; } return this.Document; }; map_docToPinMap = new Map(); map_pinHighlighted = new Map(); /* * 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 removePushpinOrRoute = (pinOrRouteDoc: Doc) => this.removeMapDocument(pinOrRouteDoc, 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.selectedPinOrRoute = undefined; }; @action deleteSelectedPinOrRoute = undoable(() => { console.log('deleting'); if (this.selectedPinOrRoute) { // Removes filter 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.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) { if (target.id === 'route-destination-searcher-listbox') return; if (target === MapAnchorMenu.top.current) return; target = target.parentElement; } e.stopPropagation(); e.preventDefault(); MapAnchorMenu.Instance.fadeOut(true); runInAction(() => { this.temporaryRouteSource = { type: 'FeatureCollection', features: [], }; }); document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); }; @action centerOnSelectedPin = () => { 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); }; /** * 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, }, }; 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.Document.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.Document.latitude, lng: this.Document.longitude, zoom: this.Document.map_zoom, mapType: this.Document.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 => { // move event if (!dragClone) { dragClone = this._dragRef.current?.cloneNode(true) as HTMLDivElement; // copy draggable pin dragClone.style.position = 'absolute'; dragClone.style.zIndex = '10000'; 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); // element for specified x and y coordinates while (target) { if (target === this._ref.current) { const cpt = this.ScreenToLocalBoxXf().transformPoint(e.clientX, e.clientY); const x = cpt[0] - (this._props.PanelWidth() - this.sidebarWidth()) / 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; } target = target.parentElement; } }, e => { const createPin = () => this.createPushpin(this.Document.latitude, this.Document.longitude, this.Document.map); if (this.bingSearchBarContents) { this.bingSearch().then(createPin); } else createPin(); } ); }; // 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[], originName: string, destination: any, createPinForDestination: boolean) => { if (originName !== destination.place_name) { const mapRoute = Docs.Create.MapRouteDocument(false, [], { title: `${originName} --> ${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); } this.temporaryRouteSource = { type: 'FeatureCollection', features: [], }; MapAnchorMenu.Instance.fadeOut(true); return mapRoute; } // TODO: Display error that can't create route to same location }, '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) => { this.featuresFromGeocodeResults = []; this.settingsOpen = false; 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.Reset(); 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.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, 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; MapAnchorMenu.Instance.Reset(); // 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.AddNewRouteToMap = this.createMapRoute; MapAnchorMenu.Instance.CreatePin = this.addMarkerForFeature; MapAnchorMenu.Instance.setMenuType('standard'); // MapAnchorMenu.Instance.jumpTo(NumCast(pinDoc.longitude), NumCast(pinDoc.latitude)-3, true); MapAnchorMenu.Instance.jumpTo(e.originalEvent.clientX, e.originalEvent.clientY, true); document.addEventListener('pointerdown', this.tryHideMapAnchorMenu, true); // this._mapRef.current.flyTo({ // center: [NumCast(pinDoc.longitude), NumCast(pinDoc.latitude)-3] // }) }; @observable temporaryRouteSource: FeatureCollection = { type: 'FeatureCollection', features: [], }; @action displayRoute = (routeInfoMap: Record | undefined, type: TransportationType) => { if (routeInfoMap) { const newTempRouteSource: FeatureCollection = { type: 'FeatureCollection', features: [ { type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: routeInfoMap[type].coordinates, }, }, ], }; // TODO: Create pin for destination // TODO: Fly to point where full route will be shown this.temporaryRouteSource = newTempRouteSource; } }; @observable isAnimating: 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; }; @observable animationUtility: AnimationUtility | null = null; @action setAnimationUtility = (util: AnimationUtility) => { this.animationUtility = util; }; @action openAnimationPanel = (routeDoc: Doc | undefined) => { if (routeDoc) { MapAnchorMenu.Instance.fadeOut(true); document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); this.featuresFromGeocodeResults = []; this.routeToAnimate = routeDoc; } }; @computed get mapboxMapViewState(): ViewState { return { zoom: NumCast(this.dataDoc.map_zoom, 8), longitude: NumCast(this.dataDoc.longitude, -71.4128), latitude: NumCast(this.dataDoc.latitude, 41.824), pitch: NumCast(this.dataDoc.map_pitch), bearing: NumCast(this.dataDoc.map_bearing), padding: { top: 0, bottom: 0, left: 0, right: 0, }, }; } @computed get preAnimationViewState() { if (!this.isAnimating) { return this.mapboxMapViewState; } } @observable isStreetViewAnimation: boolean = false; @observable animationSpeed: AnimationSpeed = AnimationSpeed.MEDIUM; @observable animationLineColor: string = '#ffff00'; @action setAnimationLineColor = (color: ColorResult) => { this.animationLineColor = color.hex; }; @action updateAnimationSpeed = () => { let newAnimationSpeed: AnimationSpeed; switch (this.animationSpeed) { case AnimationSpeed.SLOW: newAnimationSpeed = AnimationSpeed.MEDIUM; break; case AnimationSpeed.MEDIUM: newAnimationSpeed = AnimationSpeed.FAST; break; case AnimationSpeed.FAST: newAnimationSpeed = AnimationSpeed.SLOW; break; default: newAnimationSpeed = AnimationSpeed.MEDIUM; break; } this.animationSpeed = newAnimationSpeed; if (this.animationUtility) { this.animationUtility.updateAnimationSpeed(newAnimationSpeed); } }; @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 = () => { const newVal = !this.isStreetViewAnimation; this.isStreetViewAnimation = newVal; if (this.animationUtility) { this.animationUtility.updateIsStreetViewAnimation(newVal); } }; @observable dynamicRouteFeature: Feature = { type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: [], }, }; @observable path: turf.helpers.Feature = { type: 'Feature', geometry: { type: 'LineString', coordinates: [], }, properties: {}, }; getFeatureFromRouteDoc = (routeDoc: Doc): Feature => { 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; } 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.isAnimating = true; runInAction(() => { return new Promise(async resolve => { const targetLngLat = { lng: this.selectedRouteCoordinates[0][0], lat: this.selectedRouteCoordinates[0][1], }; const animationUtil = new AnimationUtility(targetLngLat, this.selectedRouteCoordinates, this.isStreetViewAnimation, this.animationSpeed, this.showTerrain, this._mapRef.current); runInAction(() => { this.setAnimationUtility(animationUtil); }); 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(() => { this.isStreetViewAnimation = false; resolve(); }, 10000); }); }); }; @action pauseAnimation = () => { if (this.frameId && this.animationPhase > 0) { window.cancelAnimationFrame(this.frameId); this.frameId = null; this.isAnimating = false; } }; @action stopAnimation = (close: boolean) => { if (this.frameId) { window.cancelAnimationFrame(this.frameId); } this.animationPhase = 0; this.frameId = null; this.finishedFlyTo = false; this.isAnimating = false; if (close) { this.animationSpeed = AnimationSpeed.MEDIUM; this.isStreetViewAnimation = false; this.routeToAnimate = undefined; this.animationUtility = null; } }; getRouteAnimationOptions = (): JSX.Element => { return ( <> { 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 ? : } color="black" size={Size.MEDIUM} /> {this.isAnimating && this.finishedFlyTo && ( { this.stopAnimation(false); this.playAnimation(AnimationStatus.START); }} icon={} color="black" size={Size.MEDIUM} /> )} this.stopAnimation(true)} icon={} color="black" size={Size.MEDIUM} /> <>
|
} />
|
|
Select Line Color:
this.setAnimationLineColor(color)} />
); }; @action hideRoute = () => { this.temporaryRouteSource = { type: 'FeatureCollection', features: [], }; }; @observable settingsOpen: boolean = false; @observable mapStyle: string = 'mapbox://styles/mapbox/standard'; @observable showTerrain: boolean = true; @action toggleSettings = () => { if (!this.isAnimating && this.animationPhase == 0) { this.featuresFromGeocodeResults = []; this.settingsOpen = !this.settingsOpen; } }; @action changeMapStyle = (e: React.ChangeEvent) => { this.dataDoc.map_style = e.target.value; // this.mapStyle = `mapbox://styles/mapbox/${e.target.value}` }; @action onBearingChange = (e: React.ChangeEvent) => { const bearing = parseInt(e.target.value); if (!isNaN(bearing) && this._mapRef.current) { console.log('bearing change'); const fixedBearing = Math.max(0, Math.min(360, bearing)); this._mapRef.current.setBearing(fixedBearing); this.dataDoc.map_bearing = fixedBearing; } }; @action onPitchChange = (e: React.ChangeEvent) => { const pitch = parseInt(e.target.value); if (!isNaN(pitch) && this._mapRef.current) { console.log('pitch change'); const fixedPitch = Math.max(0, Math.min(85, pitch)); this._mapRef.current.setPitch(fixedPitch); this.dataDoc.map_pitch = fixedPitch; } }; @action onZoomChange = (e: React.ChangeEvent) => { const zoom = parseInt(e.target.value); if (!isNaN(zoom) && this._mapRef.current) { const fixedZoom = Math.max(0, Math.min(16, zoom)); this._mapRef.current.setZoom(fixedZoom); this.dataDoc.map_zoom = fixedZoom; } }; @action onStepZoomChange = (increment: boolean) => { if (this._mapRef.current) { let newZoom: number; if (increment) { console.log('inc'); newZoom = Math.min(16, this.mapboxMapViewState.zoom + 1); } else { console.log('dec'); newZoom = Math.max(0, this.mapboxMapViewState.zoom - 1); } this._mapRef.current.setZoom(newZoom); this.dataDoc.map_zoom = newZoom; } }; @action onMapZoom = (e: ViewStateChangeEvent) => (this.dataDoc.map_zoom = e.viewState.zoom); @action onMapMove = (e: ViewStateChangeEvent) => { this.dataDoc.longitude = e.viewState.longitude; this.dataDoc.latitude = e.viewState.latitude; }; @action toggleShowTerrain = () => (this.showTerrain = !this.showTerrain); 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; 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.Document[DocCss]; if (MapBox._rerenderDelay) { // prettier-ignore this._rerenderTimeout = this._rerenderTimeout ?? setTimeout(action(() => { if ((window as any).Microsoft?.Maps?.Internal._WorkDispatcher) { MapBox._rerenderDelay = 0; } this._rerenderTimeout = undefined; this.Document[DocCss] = this.Document[DocCss] + 1; }), MapBox._rerenderDelay); return null; } const scale = this._props.NativeDimScaling?.() || 1; const parscale = scale === 1 ? 1 : this.ScreenToLocalBoxXf().Scale ?? 1; const renderAnnotations = (childFilters?: () => string[]) => null; return (
e.stopPropagation()} onPointerDown={async e => { e.button === 0 && !e.ctrlKey && e.stopPropagation(); }} style={{ transformOrigin: 'top left', transform: `scale(${scale})`, width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}>
{renderAnnotations(this.transparentFilter)}
{renderAnnotations(this.opaqueFilter)} {SnappingManager.IsDragging ? null : renderAnnotations()} {!this.routeToAnimate && (
this.handleSearchChange(e.target.value)} /> } type={Type.TERT} onClick={e => this.toggleSettings()} />
)} {this.settingsOpen && !this.routeToAnimate && (
Map Style:
Bearing:
Pitch:
Zoom:
Show terrain:
)} {this.routeToAnimate && (
{StrCast(this.routeToAnimate.title)}
{this.getRouteAnimationOptions()}
)} {this.featuresFromGeocodeResults.length > 0 && (

Choose a location for your pin:

{this.featuresFromGeocodeResults .filter(feature => feature.place_name) .map((feature, idx) => (
{ this.handleSearchChange(''); this.addMarkerForFeature(feature); }}>
{feature.place_name}
))}
)} {!this.isAnimating && this.animationPhase == 0 && } {this.routeToAnimate && (this.isAnimating || this.animationPhase > 0) && ( <> {!this.isStreetViewAnimation && ( <> )} )} <> {!this.isAnimating && this.animationPhase == 0 && this.allPushpins // .filter(anno => !anno.layout_unrendered) .map((pushpin, idx) => ( ) => this.handleMarkerClick(e, pushpin)}> {this.getMarkerIcon(pushpin)} ))} {/* {this.mapMarkers.length > 0 && this.mapMarkers.map((marker, idx) => ( ))} */}
{this.sidebarHandle}
); } }