diff options
Diffstat (limited to 'src/client/views/nodes/MapBox/MapBox.tsx')
-rw-r--r-- | src/client/views/nodes/MapBox/MapBox.tsx | 1096 |
1 files changed, 349 insertions, 747 deletions
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index 927e6fad4..d7687e03e 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ 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'; @@ -5,32 +7,30 @@ 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 mapboxgl, { LngLatBoundsLike, MapLayerMouseEvent } from 'mapbox-gl'; +import { IReactionDisposer, ObservableMap, action, autorun, computed, makeObservable, observable, 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 { ClientUtils, setupMoveUpEvents } from '../../../../ClientUtils'; +import { emptyFunction } 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 { DocCast, NumCast, StrCast, toList } from '../../../../fields/Types'; +import { DocUtils } from '../../../documents/DocUtils'; import { DocumentType } from '../../../documents/DocumentTypes'; -import { DocUtils, Docs } from '../../../documents/Documents'; -import { DocumentManager } from '../../../util/DocumentManager'; +import { Docs } from '../../../documents/Documents'; 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 { ViewBoxAnnotatableComponent } from '../../DocComponent'; +import { PinDocView, PinProps } from '../../PinFuncs'; 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 { FieldView, FieldViewProps } from '../FieldView'; +import { FocusViewOptions } from '../FocusViewOptions'; import { fastSpeedIcon, mediumSpeedIcon, slowSpeedIcon } from './AnimationSpeedIcons'; import { AnimationSpeed, AnimationStatus, AnimationUtility } from './AnimationUtility'; import { MapAnchorMenu } from './MapAnchorMenu'; @@ -53,11 +53,7 @@ import { MarkerIcons } from './MarkerIcons'; * 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=<your apikey> const MAPBOX_ACCESS_TOKEN = 'pk.eyJ1IjoiemF1bHRhdmFuZ2FyIiwiYSI6ImNscHgwNDd1MDA3MXIydm92ODdianp6cGYifQ.WFAqbhwxtMHOWSPtu0l2uQ'; -const MAPBOX_FORWARD_GEOCODE_BASE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/'; - -const MAPBOX_REVERSE_GEOCODE_BASE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/'; type PopupInfo = { longitude: number; @@ -66,72 +62,69 @@ type PopupInfo = { description: string; }; -// export type GeocoderControlProps = Omit<GeocoderOptions, 'accessToken' | 'mapboxgl' | 'marker'> & { -// mapboxAccessToken: string; -// marker?: Omit<MarkerProps, 'longitude' | 'latitude'>; -// position: ControlPosition; - -// onResult: (...args: any[]) => void; -// }; - -type MapMarker = { - longitude: number; - latitude: number; -}; - -/** - * Consider integrating later: allows for drawing, circling, making shapes on map - */ -// 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<FieldViewProps>() implements ViewBoxInterface { +export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(MapBox, fieldKey); } - private _dragRef = React.createRef<HTMLDivElement>(); + private _unmounting = false; private _sidebarRef = React.createRef<SidebarAnnos>(); private _ref: React.RefObject<HTMLDivElement> = React.createRef(); private _mapRef: React.RefObject<MapRef> = React.createRef(); private _disposers: { [key: string]: IReactionDisposer } = {}; - private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void); constructor(props: FieldViewProps) { super(props); makeObservable(this); } - @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); - @computed get allSidebarDocs() { - return DocListCast(this.dataDoc[this.SidebarKey]); - } + @observable _featuresFromGeocodeResults: any[] = []; + @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); + @observable _selectedPinOrRoute: Doc | undefined = undefined; // The pin that is selected + @observable _mapReady = false; + @observable _isAnimating: boolean = false; + @observable _routeToAnimate: Doc | undefined = undefined; + @observable _animationPhase: number = 0; + @observable _finishedFlyTo: boolean = false; + @observable _frameId: number | null = null; + @observable _animationUtility: AnimationUtility | null = null; + @observable _settingsOpen: boolean = false; + @observable _mapStyle: string = 'mapbox://styles/mapbox/standard'; + @observable _showTerrain: boolean = true; + @observable _currentPopup: PopupInfo | undefined = undefined; + @observable _isStreetViewAnimation: boolean = false; + @observable _animationSpeed: AnimationSpeed = AnimationSpeed.MEDIUM; + @observable _animationLineColor: string = '#ffff00'; + @observable _temporaryRouteSource: FeatureCollection = { type: 'FeatureCollection', features: [] }; + @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: {}, + }; + // 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 allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } // prettier-ignore + @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); } // prettier-ignore + @computed get allPushpins() { return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN); } // prettier-ignore + @computed get allRoutes() { return this.allAnnotations.filter(anno => anno.type === DocumentType.MAPROUTE); } // prettier-ignore + @computed get SidebarShown() { return !!this.layoutDoc._layout_showSidebar; } // prettier-ignore + @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } // prettier-ignore + @computed get SidebarKey() { return this.fieldKey + '_sidebar'; } // prettier-ignore + @computed get sidebarColor() { + return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this._props.fieldKey + '_backgroundColor'], '#e4e4e4')); } @computed get updatedRouteCoordinates(): Feature<Geometry, GeoJsonProperties> { - if (this.routeToAnimate?.routeCoordinates) { - const originalCoordinates: Position[] = JSON.parse(StrCast(this.routeToAnimate.routeCoordinates)); + 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 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<Geometry, GeoJsonProperties>; @@ -147,7 +140,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem feature = { type: 'Feature', properties: { - routeTitle: StrCast(this.routeToAnimate.title), + routeTitle: StrCast(this._routeToAnimate.title), }, geometry: geometry, }; @@ -158,9 +151,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem const fraction = index - startIndex; const interpolator = d3.interpolateArray(startCoord, endCoord); - const interpolatedCoord = interpolator(fraction); - const coordinates = originalCoordinates.slice(0, startIndex + 1).concat([interpolatedCoord]); geometry = { @@ -170,14 +161,14 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem feature = { type: 'Feature', properties: { - routeTitle: StrCast(this.routeToAnimate.title), + routeTitle: StrCast(this._routeToAnimate.title), }, geometry: geometry, }; } autorun(() => { - const animationUtil = this.animationUtility; + const animationUtil = this._animationUtility; const concattedCoordinates = geometry.coordinates.concat(originalCoordinates.slice(endIndex)); const newFeature: Feature<LineString, turf.Properties> = { type: 'Feature', @@ -204,11 +195,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }; } @computed get selectedRouteCoordinates(): Position[] { - let coordinates: Position[] = []; - if (this.routeToAnimate?.routeCoordinates) { - coordinates = JSON.parse(StrCast(this.routeToAnimate.routeCoordinates)); - } - return coordinates; + return !this._routeToAnimate?.routeCoordinates ? [] : JSON.parse(StrCast(this._routeToAnimate.routeCoordinates)); } @computed get allRoutesGeoJson(): FeatureCollection { @@ -233,50 +220,34 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }; } - @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 { + componentWillUnmount() { 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 docs * @param sidebarKey * @returns */ - sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { + sidebarAddDocument = (docs: 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; + toList(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)) { + if (!Doc.Links(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; @@ -284,14 +255,17 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem } }); } - }); //add to annotation list + }); // add to annotation list - return this.addDocument(doc, sidebarKey); // add to sidebar list + return this.addDocument(docs, 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)); + this.allAnnotations + .filter(anno => toList(doc).includes(DocCast(anno.mapPin))) + .forEach(anno => { + anno.mapPin = undefined; + }); return this.removeDocument(doc, annotationKey, undefined); }; @@ -311,7 +285,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem setupMoveUpEvents( this, e, - (e, down, delta) => + (moveEv, down, delta) => runInAction(() => { const localDelta = this._props .ScreenToLocalTransform() @@ -351,7 +325,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} onPointerDown={this.sidebarBtnDown}> - <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> + <FontAwesomeIcon style={{ color: Colors.WHITE }} icon="comment-alt" size="sm" /> </div> ); } @@ -370,26 +344,26 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem 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; + 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); + Doc.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; + dragComplete: dragEv => { + if (!dragEv.aborted && dragEv.annoDragData && dragEv.annoDragData.linkSourceDoc && dragEv.annoDragData.dropDocument && dragEv.linkDocument) { + dragEv.annoDragData.linkSourceDoc.followLinkToggle = dragEv.annoDragData.dropDocument.annotationOn === this.Document; + dragEv.annoDragData.linkSourceDoc.followLinkZoom = false; } }, }); @@ -399,10 +373,10 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem 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; + if (note && this._selectedPinOrRoute) { + note.latitude = this._selectedPinOrRoute.latitude; + note.longitude = this._selectedPinOrRoute.longitude; + note.map = this._selectedPinOrRoute.map; } }), 'create note annotation' @@ -415,7 +389,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem sidebarDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), true); }; - sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { + sidebarMove = (e: PointerEvent) => { 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%'; @@ -423,68 +397,20 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem return false; }; - setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => (this._setPreviewCursor = func); - - addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => this.addDocument(doc, annotationKey); - pointerEvents = () => (this._props.isContentActive() && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : 'none'); - panelWidth = () => this._props.PanelWidth() / (this._props.NativeDimScaling?.() || 1) - this.sidebarWidth(); panelHeight = () => this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1); scrollXf = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); - transparentFilter = () => [...this._props.childFilters(), Utils.TransparentBackgroundFilter]; - opaqueFilter = () => [...this._props.childFilters(), Utils.OpaqueBackgroundFilter]; + transparentFilter = () => [...this._props.childFilters(), ClientUtils.TransparentBackgroundFilter]; + opaqueFilter = () => [...this._props.childFilters(), ClientUtils.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) { + if (this._selectedPinOrRoute) { // // Removes filter // Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'remove'); // Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'remove'); @@ -509,79 +435,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem this.toggleSidebar(); options.didMove = true; } - return new Promise<Opt<DocumentView>>(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; + return new Promise<Opt<DocumentView>>(res => { + DocumentView.addViewRenderedCb(doc, dv => res(dv)); }); }; @@ -592,20 +447,20 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem /// 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), + 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), + config_map: StrCast((existingPin ?? this._selectedPinOrRoute)?.map) || StrCast(this.dataDoc.map), layout_unrendered: true, - mapPin: existingPin ?? this.selectedPinOrRoute, + 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); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), map: true } }, this.Document); return anchor; } return this.Document; @@ -613,25 +468,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem map_docToPinMap = new Map<Doc, any>(); map_pinHighlighted = new Map<Doc, boolean>(); - /* - * Input: pin doc - * Adds MicrosoftMaps Pushpin to the map (render) - */ - @action - addPushpin = (pin: Doc) => { - const pushPin = pin.infoWindowOpen - ? new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(pin.latitude, pin.longitude), {}) - : new this.MicrosoftMaps.Pushpin( - new this.MicrosoftMaps.Location(pin.latitude, pin.longitude) - // {icon: 'http://icons.iconarchive.com/icons/icons-land/vista-map-markers/24/Map-Marker-Marker-Outside-Chartreuse-icon.png'} - ); - - this._bingMap.current.entities.push(pushPin); - - this.MicrosoftMaps.Events.addHandler(pushPin, 'click', (e: any) => this.pushpinClicked(pin)); - // this.MicrosoftMaps.Events.addHandler(pushPin, 'dblclick', (e: any) => this.pushpinDblClicked(pushPin, pin)); - this.map_docToPinMap.set(pin, pushPin); - }; /* * Input: pin doc @@ -640,27 +476,16 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem @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) { + 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'); + 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); + this.removePushpinOrRoute(this._selectedPinOrRoute); } MapAnchorMenu.Instance.fadeOut(true); document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); @@ -677,7 +502,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem e.preventDefault(); MapAnchorMenu.Instance.fadeOut(true); runInAction(() => { - this.temporaryRouteSource = { + this._temporaryRouteSource = { type: 'FeatureCollection', features: [], }; @@ -688,9 +513,9 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem @action centerOnSelectedPin = () => { - if (this.selectedPinOrRoute) { + if (this._selectedPinOrRoute) { this._mapRef.current?.flyTo({ - center: [NumCast(this.selectedPinOrRoute.longitude), NumCast(this.selectedPinOrRoute.latitude)], + center: [NumCast(this._selectedPinOrRoute.longitude), NumCast(this._selectedPinOrRoute.latitude)], }); } // if (this.selectedPin) { @@ -703,33 +528,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem 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, - }, - }; - + // eslint-disable-next-line @typescript-eslint/no-unused-vars recolorPin = (pin: Doc, color?: string) => { // this._bingMap.current.entities.remove(this.map_docToPinMap.get(pin)); // this.map_docToPinMap.delete(pin); @@ -739,109 +538,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem // 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 @@ -880,20 +576,25 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem if (createPinForDestination) { this.createPushpin(destination.center[1], destination.center[0], destination.place_name); } - this.temporaryRouteSource = { + this._temporaryRouteSource = { type: 'FeatureCollection', features: [], }; MapAnchorMenu.Instance.fadeOut(true); return mapRoute; } + return undefined; // TODO: Display error that can't create route to same location }, 'createmaproute'); - searchbarKeyDown = (e: any) => e.key === 'Enter' && this.bingSearch(); - - @observable - featuresFromGeocodeResults: any[] = []; + @action + searchbarKeyDown = (e: any) => { + if (e.key === 'Enter' && this._featuresFromGeocodeResults) { + const center = this._featuresFromGeocodeResults[0]?.center; + this._featuresFromGeocodeResults = []; + setTimeout(() => center && this._mapRef.current?.flyTo({ center })); + } + }; @action addMarkerForFeature = (feature: any) => { @@ -910,7 +611,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem center: feature.center, }); } - this.featuresFromGeocodeResults = []; + this._featuresFromGeocodeResults = []; } else { // TODO: handle error } @@ -922,11 +623,11 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem */ handleSearchChange = async (searchText: string) => { const features = await MapboxApiUtility.forwardGeocodeForFeatures(searchText); - if (features && !this.isAnimating) { + if (features && !this._isAnimating) { runInAction(() => { - this.settingsOpen = false; - this.featuresFromGeocodeResults = features; - this.routeToAnimate = undefined; + this._settingsOpen = false; + this._featuresFromGeocodeResults = features; + this._routeToAnimate = undefined; }); } // try { @@ -946,8 +647,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem @action handleMapClick = (e: MapLayerMouseEvent) => { - this.featuresFromGeocodeResults = []; - this.settingsOpen = false; + this._featuresFromGeocodeResults = []; + this._settingsOpen = false; if (this._mapRef.current) { const features = this._mapRef.current.queryRenderedFeatures(e.point, { layers: ['map-routes-layer'], @@ -955,13 +656,12 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem 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); + const { routeTitle } = features[0].properties; + const routeDoc: Doc | undefined = this.allRoutes.find(rtDoc => rtDoc.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'); + this._selectedPinOrRoute = routeDoc; + Doc.setDocFilter(this.Document, LinkedTo, `mapRoute=${Field.toScriptString(this._selectedPinOrRoute)}`, 'check'); // TODO: Recolor route @@ -1001,14 +701,14 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem */ handleMapDblClick = async (e: MapLayerMouseEvent) => { e.preventDefault(); - const lngLat: LngLat = e.lngLat; + const { lngLat } = e; const longitude: number = lngLat.lng; const latitude: number = lngLat.lat; const features = await MapboxApiUtility.reverseGeocodeForFeatures(longitude, latitude); if (features) { runInAction(() => { - this.featuresFromGeocodeResults = features; + this._featuresFromGeocodeResults = features; }); } @@ -1028,21 +728,18 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem // } }; - @observable - currentPopup: PopupInfo | undefined = undefined; - @action handleMarkerClick = (e: MarkerEvent<mapboxgl.Marker, MouseEvent>, pinDoc: Doc) => { - this.featuresFromGeocodeResults = []; + this._featuresFromGeocodeResults = []; this.deselectPinOrRoute(); // TODO: check this method - this.selectedPinOrRoute = pinDoc; + 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'); + Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(this._selectedPinOrRoute)}`, 'check'); - this.recolorPin(this.selectedPinOrRoute, 'green'); // TODO: check this method + this.recolorPin(this._selectedPinOrRoute, 'green'); // TODO: check this method MapAnchorMenu.Instance.Delete = this.deleteSelectedPinOrRoute; MapAnchorMenu.Instance.Center = this.centerOnSelectedPin; @@ -1072,12 +769,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem // }) }; - @observable - temporaryRouteSource: FeatureCollection = { - type: 'FeatureCollection', - features: [], - }; - @action displayRoute = (routeInfoMap: Record<TransportationType, any> | undefined, type: TransportationType) => { if (routeInfoMap) { @@ -1096,41 +787,23 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }; // TODO: Create pin for destination // TODO: Fly to point where full route will be shown - this.temporaryRouteSource = newTempRouteSource; + 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; + this._animationPhase = newValue; }; - @observable - frameId: number | null = null; - @action setFrameId = (frameId: number) => { - this.frameId = frameId; + this._frameId = frameId; }; - @observable - animationUtility: AnimationUtility | null = null; - @action setAnimationUtility = (util: AnimationUtility) => { - this.animationUtility = util; + this._animationUtility = util; }; @action @@ -1138,8 +811,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem if (routeDoc) { MapAnchorMenu.Instance.fadeOut(true); document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); - this.featuresFromGeocodeResults = []; - this.routeToAnimate = routeDoc; + this._featuresFromGeocodeResults = []; + this._routeToAnimate = routeDoc; } }; @@ -1161,29 +834,21 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem @computed get preAnimationViewState() { - if (!this.isAnimating) { + if (!this._isAnimating) { return this.mapboxMapViewState; } + return undefined; } - @observable - isStreetViewAnimation: boolean = false; - - @observable - animationSpeed: AnimationSpeed = AnimationSpeed.MEDIUM; - - @observable - animationLineColor: string = '#ffff00'; - @action setAnimationLineColor = (color: ColorResult) => { - this.animationLineColor = color.hex; + this._animationLineColor = color.hex; }; @action updateAnimationSpeed = () => { let newAnimationSpeed: AnimationSpeed; - switch (this.animationSpeed) { + switch (this._animationSpeed) { case AnimationSpeed.SLOW: newAnimationSpeed = AnimationSpeed.MEDIUM; break; @@ -1197,63 +862,33 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem newAnimationSpeed = AnimationSpeed.MEDIUM; break; } - this.animationSpeed = newAnimationSpeed; - if (this.animationUtility) { - this.animationUtility.updateAnimationSpeed(newAnimationSpeed); + 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'; - } + 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'; + } // prettier-ignore } @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; - } + switch (this._animationSpeed) { + case AnimationSpeed.SLOW: return slowSpeedIcon; + case AnimationSpeed.MEDIUM: return mediumSpeedIcon; + case AnimationSpeed.FAST: return fastSpeedIcon; + default: return mediumSpeedIcon; + } // prettier-ignore } @action toggleIsStreetViewAnimation = () => { - const newVal = !this.isStreetViewAnimation; - this.isStreetViewAnimation = newVal; - if (this.animationUtility) { - this.animationUtility.updateIsStreetViewAnimation(newVal); - } - }; - - @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: {}, + const newVal = !this._isStreetViewAnimation; + this._isStreetViewAnimation = newVal; + this._animationUtility?.updateIsStreetViewAnimation(newVal); }; getFeatureFromRouteDoc = (routeDoc: Doc): Feature<Geometry, GeoJsonProperties> => { @@ -1272,190 +907,172 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem @action playAnimation = (status: AnimationStatus) => { - if (!this._mapRef.current || !this.routeToAnimate) { + 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; + 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._settingsOpen = false; this.path = path; - this.isAnimating = true; - - runInAction(() => { - return new Promise<void>(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); - }); + this._isAnimating = true; + + runInAction( + () => + // eslint-disable-next-line no-async-promise-executor + new Promise<void>(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, + }); - const updateFrameId = (newFrameId: number) => { - this.setFrameId(newFrameId); - }; + console.log('Bearing: ', result.bearing); + console.log('Altitude: ', result.altitude); + } - const updateAnimationPhase = (newAnimationPhase: number) => { - this.setAnimationPhase(newAnimationPhase); - }; + runInAction(() => { + this._finishedFlyTo = true; + }); - if (status !== AnimationStatus.RESUME) { - const result = await animationUtil.flyInAndRotate({ + // 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!, - // targetLngLat, - // duration 3000 - // startAltitude: 3000000, - // endAltitude: this.isStreetViewAnimation ? 80 : 12000, - // startBearing: 0, - // endBearing: -20, - // startPitch: 40, - // endPitch: this.isStreetViewAnimation ? 80 : 50, + // path: this.path, + // startBearing: -20, + // startAltitude: this.isStreetViewAnimation ? 80 : 12000, + // pitch: this.isStreetViewAnimation ? 80: 50, + currentAnimationPhase: this._animationPhase, + updateAnimationPhase, 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); + // 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]]; + const bbox2d: LngLatBoundsLike = [bbox3d[0], bbox3d[1], bbox3d[2], bbox3d[3]]; - this._mapRef.current!.fitBounds(bbox2d, { - duration: 3000, - pitch: 30, - bearing: 0, - padding: 120, - }); + this._mapRef.current!.fitBounds(bbox2d, { + duration: 3000, + pitch: 30, + bearing: 0, + padding: 120, + }); - setTimeout(() => { - this.isStreetViewAnimation = false; - resolve(); - }, 10000); - }); - }); + setTimeout(() => { + this._isStreetViewAnimation = false; + resolve(); + }, 10000); + }) + ); }; @action pauseAnimation = () => { - if (this.frameId && this.animationPhase > 0) { - window.cancelAnimationFrame(this.frameId); - this.frameId = null; - this.isAnimating = false; + 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); + if (this._frameId) { + window.cancelAnimationFrame(this._frameId); } - this.animationPhase = 0; - this.frameId = null; - this.finishedFlyTo = false; - this.isAnimating = false; + 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; + this._animationSpeed = AnimationSpeed.MEDIUM; + this._isStreetViewAnimation = false; + this._routeToAnimate = undefined; + this._animationUtility = null; } }; - getRouteAnimationOptions = (): JSX.Element => { - return ( - <> + getRouteAnimationOptions = (): JSX.Element => ( + <> + <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={this.isAnimating && this.finishedFlyTo ? 'Pause Animation' : 'Play Animation'} + tooltip="Restart 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 - } + this.stopAnimation(false); + this.playAnimation(AnimationStatus.START); }} - icon={this.isAnimating && this.finishedFlyTo ? <FontAwesomeIcon icon={faPause as IconLookup} /> : <FontAwesomeIcon icon={faPlay as IconLookup} />} + icon={<FontAwesomeIcon icon={faRotate as IconLookup} />} color="black" size={Size.MEDIUM} /> - {this.isAnimating && this.finishedFlyTo && ( - <IconButton - tooltip="Restart animation" - onPointerDown={() => { - this.stopAnimation(false); - this.playAnimation(AnimationStatus.START); - }} - icon={<FontAwesomeIcon icon={faRotate as IconLookup} />} - color="black" - size={Size.MEDIUM} - /> - )} - <IconButton style={{ marginRight: '10px' }} tooltip="Stop and close animation" onPointerDown={() => this.stopAnimation(true)} icon={<FontAwesomeIcon icon={faCircleXmark as IconLookup} />} color="black" size={Size.MEDIUM} /> - <> - <div className="animation-suboptions"> - <div>|</div> - <FormControlLabel className="first-person-label" label="1st person animation:" labelPlacement="start" control={<Checkbox color="success" checked={this.isStreetViewAnimation} onChange={this.toggleIsStreetViewAnimation} />} /> - <div id="divider">|</div> - <IconButton tooltip={this.animationSpeedTooltipText} onPointerDown={this.updateAnimationSpeed} icon={this.animationSpeedIcon} size={Size.MEDIUM} /> - <div id="divider">|</div> - <div style={{ display: 'flex', alignItems: 'center' }}> - <div>Select Line Color: </div> - <CirclePicker circleSize={12} circleSpacing={5} width="100%" colors={['#ffff00', '#03a9f4', '#ff0000', '#ff5722', '#000000', '#673ab7']} onChange={(color: any) => this.setAnimationLineColor(color)} /> - </div> - </div> - </> - </> - ); - }; + )} + <IconButton style={{ marginRight: '10px' }} tooltip="Stop and close animation" onPointerDown={() => this.stopAnimation(true)} icon={<FontAwesomeIcon icon={faCircleXmark as IconLookup} />} color="black" size={Size.MEDIUM} /> + <div className="animation-suboptions"> + <div>|</div> + <FormControlLabel className="first-person-label" label="1st person animation:" labelPlacement="start" control={<Checkbox color="success" checked={this._isStreetViewAnimation} onChange={this.toggleIsStreetViewAnimation} />} /> + <div id="divider">|</div> + <IconButton tooltip={this.animationSpeedTooltipText} onPointerDown={this.updateAnimationSpeed} icon={this.animationSpeedIcon} size={Size.MEDIUM} /> + <div id="divider">|</div> + <div style={{ display: 'flex', alignItems: 'center' }}> + <div>Select Line Color: </div> + <CirclePicker circleSize={12} circleSpacing={5} width="100%" colors={['#ffff00', '#03a9f4', '#ff0000', '#ff5722', '#000000', '#673ab7']} onChange={(color: any) => this.setAnimationLineColor(color)} /> + </div> + </div> + </> + ); @action hideRoute = () => { - this.temporaryRouteSource = { + 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; + if (!this._isAnimating && this._animationPhase === 0) { + this._featuresFromGeocodeResults = []; + this._settingsOpen = !this._settingsOpen; } }; @@ -1500,21 +1117,19 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem @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); - } + const newZoom = increment // + ? Math.min(16, this.mapboxMapViewState.zoom + 1) + : 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); + onMapZoom = (e: ViewStateChangeEvent) => { + this.dataDoc.map_zoom = e.viewState.zoom; + }; @action onMapMove = (e: ViewStateChangeEvent) => { @@ -1523,7 +1138,9 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }; @action - toggleShowTerrain = () => (this.showTerrain = !this.showTerrain); + toggleShowTerrain = () => { + this._showTerrain = !this._showTerrain; + }; getMarkerIcon = (pinDoc: Doc): JSX.Element | null => { const markerType = StrCast(pinDoc.markerType); @@ -1532,48 +1149,28 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem return MarkerIcons.getFontAwesomeIcon(markerType, '2x', markerColor) ?? null; }; - static _firstRender = true; - static _rerenderDelay = 500; - _rerenderTimeout: any; + _textRef = React.createRef<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 ( <div className="mapBox" ref={this._ref}> <div className="mapBox-wrapper" onWheel={e => e.stopPropagation()} - onPointerDown={async e => { - e.button === 0 && !e.ctrlKey && e.stopPropagation(); - }} + onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()} style={{ transformOrigin: 'top left', transform: `scale(${scale})`, width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> - <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div> - {renderAnnotations(this.opaqueFilter)} - {SnappingManager.IsDragging ? null : renderAnnotations()} - {!this.routeToAnimate && ( + {!this._routeToAnimate && ( <div className="mapBox-searchbar" style={{ width: `${100 / scale}%`, zIndex: 1, position: 'relative', background: 'lightGray' }}> - <TextField fullWidth placeholder="Enter a location" onChange={(e: any) => this.handleSearchChange(e.target.value)} /> - <IconButton icon={<FontAwesomeIcon icon={faGear as IconLookup} size="1x" />} type={Type.TERT} onClick={e => this.toggleSettings()} /> + <TextField ref={this._textRef} fullWidth placeholder="Enter a location" onKeyDown={this.searchbarKeyDown} onChange={(e: any) => this.handleSearchChange(e.target.value)} /> + <IconButton icon={<FontAwesomeIcon icon={faGear as IconLookup} size="1x" />} type={Type.TERT} onClick={() => this.toggleSettings()} /> + <div style={{ opacity: 0 }}> + <IconButton icon={<FontAwesomeIcon icon={faGear as IconLookup} size="1x" />} type={Type.TERT} onClick={() => this.toggleSettings()} /> + </div> </div> )} - {this.settingsOpen && !this.routeToAnimate && ( + {this._settingsOpen && !this._routeToAnimate && ( <div className="mapbox-settings-panel" style={{ right: `${0 + this.sidebarWidth()}px` }}> <div className="mapbox-style-select"> <div>Map Style:</div> @@ -1605,41 +1202,40 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem </div> <div className="mapbox-terrain-selection"> <div>Show terrain: </div> - <input type="checkbox" checked={this.showTerrain} onChange={this.toggleShowTerrain} /> + <input type="checkbox" checked={this._showTerrain} onChange={this.toggleShowTerrain} /> </div> </div> )} - {this.routeToAnimate && ( + {this._routeToAnimate && ( <div className="animation-panel" style={{ width: this.sidebarWidth() === 0 ? '100%' : `calc(100% - ${this.sidebarWidth()}px)` }}> - <div id="route-to-animate-title">{StrCast(this.routeToAnimate.title)}</div> + <div id="route-to-animate-title">{StrCast(this._routeToAnimate.title)}</div> <div className="route-animation-options">{this.getRouteAnimationOptions()}</div> </div> )} - {this.featuresFromGeocodeResults.length > 0 && ( + {this._featuresFromGeocodeResults.length > 0 && ( <div className="mapbox-geocoding-search-results"> - <React.Fragment> - <h4>Choose a location for your pin: </h4> - {this.featuresFromGeocodeResults - .filter(feature => feature.place_name) - .map((feature, idx) => ( - <div - key={idx} - className="search-result-container" - onClick={() => { - this.handleSearchChange(''); - this.addMarkerForFeature(feature); - }}> - <div className="search-result-place-name">{feature.place_name}</div> - </div> - ))} - </React.Fragment> + <h4>Choose a location for your pin: </h4> + {this._featuresFromGeocodeResults + .filter(feature => feature.place_name) + .map((feature, idx) => ( + <div + // eslint-disable-next-line react/no-array-index-key + key={idx} + className="search-result-container" + onClick={() => { + this.handleSearchChange(''); + this.addMarkerForFeature(feature); + }}> + <div className="search-result-place-name">{feature.place_name}</div> + </div> + ))} </div> )} <MapProvider> <MapboxMap ref={this._mapRef} mapboxAccessToken={MAPBOX_ACCESS_TOKEN} - viewState={this.isAnimating || this.routeToAnimate ? undefined : { ...this.mapboxMapViewState, width: NumCast(this.layoutDoc._width), height: NumCast(this.layoutDoc._height) }} + viewState={this._isAnimating || this._routeToAnimate ? undefined : { ...this.mapboxMapViewState, width: NumCast(this.layoutDoc._width), height: NumCast(this.layoutDoc._height) }} mapStyle={this.dataDoc.map_style ? StrCast(this.dataDoc.map_style) : 'mapbox://styles/mapbox/streets-v11'} style={{ position: 'absolute', @@ -1649,20 +1245,22 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem width: NumCast(this.layoutDoc._width) * parscale, height: NumCast(this.layoutDoc._height) * parscale, }} - initialViewState={this.isAnimating ? undefined : this.mapboxMapViewState} + initialViewState={this._isAnimating ? undefined : this.mapboxMapViewState} onZoom={this.onMapZoom} onMove={this.onMapMove} onClick={this.handleMapClick} onDblClick={this.handleMapDblClick} - terrain={this.showTerrain ? { source: 'mapbox-dem', exaggeration: 2.0 } : undefined}> + terrain={this._showTerrain ? { source: 'mapbox-dem', exaggeration: 2.0 } : undefined}> <Source id="mapbox-dem" type="raster-dem" url="mapbox://mapbox.mapbox-terrain-dem-v1" tileSize={512} maxzoom={14} /> - <Source id="temporary-route" type="geojson" data={this.temporaryRouteSource} /> + <Source id="temporary-route" type="geojson" data={this._temporaryRouteSource} /> <Source id="map-routes" type="geojson" data={this.allRoutesGeoJson} /> <Layer id="temporary-route-layer" type="line" source="temporary-route" layout={{ 'line-join': 'round', 'line-cap': 'round' }} paint={{ 'line-color': '#36454F', 'line-width': 4, 'line-dasharray': [1, 1] }} /> - {!this.isAnimating && this.animationPhase == 0 && <Layer id="map-routes-layer" type="line" source="map-routes" layout={{ 'line-join': 'round', 'line-cap': 'round' }} paint={{ 'line-color': '#FF0000', 'line-width': 4 }} />} - {this.routeToAnimate && (this.isAnimating || this.animationPhase > 0) && ( + {!this._isAnimating && this._animationPhase === 0 && ( + <Layer id="map-routes-layer" type="line" source="map-routes" layout={{ 'line-join': 'round', 'line-cap': 'round' }} paint={{ 'line-color': '#FF0000', 'line-width': 4 }} /> + )} + {this._routeToAnimate && (this._isAnimating || this._animationPhase > 0) && ( <> - {!this.isStreetViewAnimation && ( + {!this._isStreetViewAnimation && ( <> <Source id="animated-route" type="geojson" data={this.updatedRouteCoordinates} /> <Layer @@ -1670,7 +1268,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem type="line" source="animated-route" paint={{ - 'line-color': this.animationLineColor, + 'line-color': this._animationLineColor, 'line-width': 5, }} /> @@ -1721,17 +1319,15 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem </> )} - <> - {!this.isAnimating && - this.animationPhase == 0 && - this.allPushpins - // .filter(anno => !anno.layout_unrendered) - .map((pushpin, idx) => ( - <Marker key={idx} longitude={NumCast(pushpin.longitude)} latitude={NumCast(pushpin.latitude)} anchor="bottom" onClick={(e: MarkerEvent<mapboxgl.Marker, MouseEvent>) => this.handleMarkerClick(e, pushpin)}> - {this.getMarkerIcon(pushpin)} - </Marker> - ))} - </> + {!this._isAnimating && + this._animationPhase === 0 && + this.allPushpins // .filter(anno => !anno.layout_unrendered) + .map((pushpin, idx) => ( + // eslint-disable-next-line react/no-array-index-key + <Marker key={idx} longitude={NumCast(pushpin.longitude)} latitude={NumCast(pushpin.latitude)} anchor="bottom" onClick={(e: MarkerEvent<mapboxgl.Marker, MouseEvent>) => this.handleMarkerClick(e, pushpin)}> + {this.getMarkerIcon(pushpin)} + </Marker> + ))} {/* {this.mapMarkers.length > 0 && this.mapMarkers.map((marker, idx) => ( <Marker key={idx} longitude={marker.longitude} latitude={marker.latitude}/> @@ -1742,12 +1338,13 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem <div className="mapBox-sidebar" style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> <SidebarAnnos ref={this._sidebarRef} + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} fieldKey={this.fieldKey} Document={this.Document} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} - usePanelWidth={true} + usePanelWidth showSidebar={this.SidebarShown} nativeWidth={NumCast(this.layoutDoc._nativeWidth)} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} @@ -1762,3 +1359,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem ); } } + +Docs.Prototypes.TemplateMap.set(DocumentType.MAP, { + layout: { view: MapBox, dataField: 'data' }, + options: { acl: '', map: '', _height: 600, _width: 800, _layout_reflowHorizontal: true, _layout_reflowVertical: true, _layout_nativeDimEditable: true, systemIcon: 'BsFillPinMapFill' }, +}); |