/* eslint-disable @typescript-eslint/no-explicit-any */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, EditableText, IconButton, Type } from 'browndash-components'; import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { MapProvider, Map as MapboxMap } from 'react-map-gl'; import { ClientUtils, returnEmptyFilter, returnFalse, returnOne, setupMoveUpEvents } from '../../../../ClientUtils'; import { emptyFunction } from '../../../../Utils'; import { Doc, DocListCast, Field, LinkedTo, Opt, returnEmptyDoclist } from '../../../../fields/Doc'; import { DocCss, Highlight } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { DocCast, NumCast, StrCast, toList } from '../../../../fields/Types'; import { DocUtils } from '../../../documents/DocUtils'; import { DocumentType } from '../../../documents/DocumentTypes'; import { Docs } from '../../../documents/Documents'; import { DragManager } from '../../../util/DragManager'; import { Transform } from '../../../util/Transform'; import { UndoManager, undoable } from '../../../util/UndoManager'; 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 { FieldView, FieldViewProps } from '../FieldView'; import { FocusViewOptions } from '../FocusViewOptions'; import { MapAnchorMenu } from '../MapBox/MapAnchorMenu'; import '../MapBox/MapBox.scss'; /** * MapBox architecture: * Main component: MapBox.tsx * Supporting Components: SidebarAnnos, CollectionStackingView * * MapBox is a node that extends the ViewBoxAnnotatableComponent. Similar to PDFBox and WebBox, it supports interaction between sidebar content and document content. * The main body of MapBox uses Google Maps API to allow location retrieval, adding map markers, pan and zoom, and open street view. * Dash Document architecture is integrated with Maps API: When drag and dropping documents with ExifData (gps Latitude and Longitude information) available, * sidebarAddDocument function checks if the document contains lat & lng information, if it does, then the document is added to both the sidebar and the infowindow (a pop up corresponding to a map marker--pin on map). * The lat and lng field of the document is filled when importing (spec see ConvertDMSToDD method and processFileUpload method in Documents.ts). * A map marker is considered a document that contains a collection with stacking view of documents, it has a lat, lng location, which is passed to Maps API's custom marker (red pin) to be rendered on the google maps */ const mapboxApiKey = 'pk.eyJ1IjoiemF1bHRhdmFuZ2FyIiwiYSI6ImNsbnc2eHJpbTA1ZTUyam85aGx4Z2FhbGwifQ.2Kqw9mk-9wAAg9kmHmKzcg'; /** * Consider integrating later: allows for drawing, circling, making shapes on map */ // const drawingManager = new window.google.maps.drawing.DrawingManager({ // drawingControl: true, // drawingControlOptions: { // position: google.maps.ControlPosition.TOP_RIGHT, // drawingModes: [ // google.maps.drawing.OverlayType.MARKER, // // currently we are not supporting the following drawing mode on map, a thought for future development // google.maps.drawing.OverlayType.CIRCLE, // google.maps.drawing.OverlayType.POLYLINE, // ], // }, // }); @observer export class MapBoxContainer extends ViewBoxAnnotatableComponent() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(MapBoxContainer, fieldKey); } private _dragRef = React.createRef(); private _sidebarRef = React.createRef(); private _ref: 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 SidebarShown() { return !!this.layoutDoc._layout_showSidebar; } @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.deselectPin(); this._rerenderTimeout && clearTimeout(this._rerenderTimeout); Object.keys(this._disposers).forEach(key => this._disposers[key]?.()); } /** * Called when dragging documents into map sidebar or directly into infowindow; to create a map marker, ref to MapMarkerDocument in Documents.ts * @param doc * @param sidebarKey * @returns */ sidebarAddDocument = (docsIn: Doc | Doc[], sidebarKey?: string) => { if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); const docs = toList(docsIn); docs.forEach(doc => { let existingPin = this.allPushpins.find(pin => pin.latitude === doc.latitude && pin.longitude === doc.longitude) ?? this.selectedPin; if (doc.latitude !== undefined && doc.longitude !== undefined && !existingPin) { existingPin = this.createPushpin(NumCast(doc.latitude), NumCast(doc.longitude), StrCast(doc.map)); } if (existingPin) { setTimeout(() => { // we use a timeout in case this is called from the sidebar which may have just added a link that hasn't made its way into th elink manager yet if (!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; doc.longitude = existingPin?.longitude; } }); } }); // add to annotation list return this.addDocument(docs, sidebarKey); // add to sidebar list }; removeMapDocument = (docsIn: Doc | Doc[], annotationKey?: string) => { const docs = toList(docsIn); this.allAnnotations .filter(anno => docs.includes(DocCast(anno.mapPin))) .forEach(anno => { anno.mapPin = undefined; }); return this.removeDocument(docsIn, 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, (moveEv, 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.selectedPin) { note.latitude = this.selectedPin.latitude; note.longitude = this.selectedPin.longitude; note.map = this.selectedPin.map; } return note as Doc; }); const targetCreator = (annotationOn: Doc | undefined) => { const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); Doc.SetSelectOnLoad(target); return target; }; const docView = this.DocumentView?.(); docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, { 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; } }, }); }; createNoteAnnotation = () => { const createFunc = undoable( action(() => { const note = this._sidebarRef.current?.anchorMenuClick(this.getAnchor(true), ['latitude', 'longitude', LinkedTo]); if (note && this.selectedPin) { note.latitude = this.selectedPin.latitude; note.longitude = this.selectedPin.longitude; note.map = this.selectedPin.map; } }), 'create note annotation' ); if (!this.layoutDoc.layout_showSidebar) { this.toggleSidebar(); setTimeout(createFunc); } else createFunc(); }; sidebarDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), true); }; sidebarMove = (e: PointerEvent) => { 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(), 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) => 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: () => reject(), }); } }); @observable bingSearchBarContents: any = this.Document.map; // For Bing Maps: The contents of the Bing search bar (string) geoDataRequestOptions = { entityType: 'PopulatedPlace', }; // incrementer: number = 0; /* * Creates Pushpin doc and adds it to the list of annotations */ @action createPushpin = undoable((latitude: number, longitude: number, map?: string) => { // Stores the pushpin as a MapMarkerDocument const pushpin = Docs.Create.PushpinDocument( NumCast(latitude), NumCast(longitude), false, [], { title: map ?? `lat=${latitude},lng=${longitude}`, map: map } // ,'pushpinIDamongus'+ this.incrementer++ ); this.addDocument(pushpin, this.annotationKey); return pushpin; // mapMarker.infoWindowOpen = true; }, 'createpin'); // The pin that is selected @observable selectedPin: Doc | undefined = undefined; @action deselectPin = () => { if (this.selectedPin) { // Removes filter Doc.setDocFilter(this.Document, 'latitude', NumCast(this.selectedPin.latitude), 'remove'); Doc.setDocFilter(this.Document, 'longitude', NumCast(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', () => 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 => { DocumentView.addViewRenderedCb(doc, dv => res(dv)); }); }; /* * Pushpin onclick */ @action pushpinClicked = (pinDoc: Doc) => { this.deselectPin(); this.selectedPin = 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.selectedPin)}`, 'check'); this.recolorPin(this.selectedPin, 'green'); MapAnchorMenu.Instance.Delete = this.deleteSelectedPin; MapAnchorMenu.Instance.Center = this.centerOnSelectedPin; MapAnchorMenu.Instance.OnClick = this.createNoteAnnotation; MapAnchorMenu.Instance.StartDrag = this.startAnchorDrag; const point = this._bingMap.current.tryLocationToPixel(new this.MicrosoftMaps.Location(this.selectedPin.latitude, this.selectedPin.longitude)); const x = point.x + (this._props.PanelWidth() - this.sidebarWidth()) / 2; const y = point.y + this._props.PanelHeight() / 2 + 32; const cpt = this.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.deselectPin(); }; /* * Updates values of layout doc to match the current map */ @action mapRecentered = () => { if ( Math.abs(NumCast(this.dataDoc.latitude) - this._bingMap.current.getCenter().latitude) > 1e-7 || // Math.abs(NumCast(this.dataDoc.longitude) - this._bingMap.current.getCenter().longitude) > 1e-7 ) { this.dataDoc.latitude = this._bingMap.current.getCenter().latitude; this.dataDoc.longitude = this._bingMap.current.getCenter().longitude; this.dataDoc.map = ''; this.bingSearchBarContents = ''; } this.dataDoc.map_zoom = this._bingMap.current.getZoom(); }; /* * Updates maptype */ @action updateMapType = () => { this.dataDoc.map_type = this._bingMap.current.getMapTypeId(); }; /* * For Bing Maps * Called by search button's onClick * Finds the geocode of the searched contents and sets location to that location * */ @action bingSearch = () => 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.selectedPin?.map) || StrCast(this.Document.map) || 'map location') as any, config_latitude: NumCast((existingPin ?? this.selectedPin)?.latitude ?? this.dataDoc.latitude), config_longitude: NumCast((existingPin ?? this.selectedPin)?.longitude ?? this.dataDoc.longitude), config_map_zoom: NumCast(this.dataDoc.map_zoom), config_map_type: StrCast(this.dataDoc.map_type), config_map: StrCast((existingPin ?? this.selectedPin)?.map) || StrCast(this.dataDoc.map), layout_unrendered: true, mapPin: existingPin ?? this.selectedPin, annotationOn: this.Document, }); if (anchor) { if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; addAsAnnotation && this.addDocument(anchor); 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', () => this.pushpinClicked(pin)); // this.MicrosoftMaps.Events.addHandler(pushPin, 'dblclick', (e: any) => this.pushpinDblClicked(pushPin, pin)); this.map_docToPinMap.set(pin, pushPin); }; /* * Input: pin doc * Removes pin from annotations */ @action removePushpin = (pinDoc: Doc) => this.removeMapDocument(pinDoc, this.annotationKey); /* * Removes pushpin from map render */ deletePushpin = (pinDoc: Doc) => { if (!this._unmounting) { this._bingMap.current.entities.remove(this.map_docToPinMap.get(pinDoc)); } this.map_docToPinMap.delete(pinDoc); this.selectedPin = undefined; }; @action deleteSelectedPin = undoable(() => { if (this.selectedPin) { // Removes filter Doc.setDocFilter(this.Document, 'latitude', NumCast(this.selectedPin.latitude), 'remove'); Doc.setDocFilter(this.Document, 'longitude', NumCast(this.selectedPin.longitude), 'remove'); Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove'); this.removePushpin(this.selectedPin); } MapAnchorMenu.Instance.fadeOut(true); document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); }, 'delete pin'); tryHideMapAnchorMenu = (e: PointerEvent) => { let target = document.elementFromPoint(e.x, e.y); while (target) { if (target === MapAnchorMenu.top.current) return; target = target.parentElement; } e.stopPropagation(); e.preventDefault(); MapAnchorMenu.Instance.fadeOut(true); document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); }; @action centerOnSelectedPin = () => { if (this.selectedPin) { this.dataDoc.latitude = this.selectedPin.latitude; this.dataDoc.longitude = this.selectedPin.longitude; this.dataDoc.map = this.selectedPin.map ?? ''; this.bingSearchBarContents = this.selectedPin.map; } MapAnchorMenu.Instance.fadeOut(true); document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu); }; /** * View options for bing maps */ bingViewOptions = { // center: { latitude: this.dataDoc.latitude ?? defaultCenter.lat, longitude: this.dataDoc.longitude ?? defaultCenter.lng }, zoom: this.dataDoc.latitude ?? 10, mapTypeId: 'grayscale', }; /** * Map options */ bingMapOptions = { navigationBarMode: 'square', backgroundColor: '#f1f3f4', enableInertia: true, supportedMapTypes: ['grayscale', 'canvasLight'], disableMapTypeSelectorMouseOver: true, // showScalebar:true // disableRoadView:true, // disableBirdseye:true streetsideOptions: { showProblemReporting: false, showCurrentAddress: false, }, }; @action searchbarOnEdit = (newText: string) => { this.bingSearchBarContents = newText; }; recolorPin = (pin: Doc, color?: string) => { this._bingMap.current.entities.remove(this.map_docToPinMap.get(pin)); this.map_docToPinMap.delete(pin); const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(pin.latitude, pin.longitude), color ? { color } : {}); this.MicrosoftMaps.Events.addHandler(newpin, 'click', () => 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(({ 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, moveEv => { if (!dragClone) { dragClone = this._dragRef.current?.cloneNode(true) as HTMLDivElement; dragClone.style.position = 'absolute'; dragClone.style.zIndex = '10000'; DragManager.Root().appendChild(dragClone); } dragClone.style.transform = `translate(${moveEv.clientX - 15}px, ${moveEv.clientY - 15}px)`; return false; }, upEv => { if (!dragClone) return; DragManager.Root().removeChild(dragClone); let target = document.elementFromPoint(upEv.x, upEv.y); while (target) { if (target === this._ref.current) { const cpt = this.ScreenToLocalBoxXf().transformPoint(upEv.clientX, upEv.clientY); const x = cpt[0] - (this._props.PanelWidth() - this.sidebarWidth()) / 2; const y = cpt[1] - 32 /* height of search bar */ - this._props.PanelHeight() / 2; const location = this._bingMap.current.tryPixelToLocation(new this.MicrosoftMaps.Point(x, y)); this.createPushpin(location.latitude, location.longitude); break; } target = target.parentElement; } }, () => { const createPin = () => this.createPushpin(this.Document.latitude, this.Document.longitude, this.Document.map); if (this.bingSearchBarContents) { this.bingSearch().then(createPin); } else createPin(); } ); }; searchbarKeyDown = (e: any) => e.key === 'Enter' && this.bingSearch(); static _firstRender = true; static _rerenderDelay = 500; _rerenderTimeout: any; render() { // bcz: no idea what's going on here, but bings maps have some kind of bug // such that we need to delay rendering a second map on startup until the first map is rendered. this.Document[DocCss]; if (MapBoxContainer._rerenderDelay) { // prettier-ignore this._rerenderTimeout = this._rerenderTimeout ?? setTimeout(action(() => { if ((window as any).Microsoft?.Maps?.Internal._WorkDispatcher) { MapBoxContainer._rerenderDelay = 0; } this._rerenderTimeout = undefined; // eslint-disable-next-line operator-assignment this.Document[DocCss] = this.Document[DocCss] + 1; }), MapBoxContainer._rerenderDelay); return null; } return (
e.stopPropagation()} onPointerDown={async e => { e.button === 0 && !e.ctrlKey && e.stopPropagation(); }} style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}>
typeof newText === 'string' && this.searchbarOnEdit(newText)} onEnter={() => this.bingSearch()} placeholder={this.bingSearchBarContents || 'enter city/zip/...'} textAlign="center" />
{/* */}
{!this._mapReady ? null : this.allAnnotations .filter(anno => !anno.layout_unrendered) .map(pushpin => ( ))}
{/* */}
{/* */}
{this.sidebarHandle}
); } }