diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/views/nodes/MapBox/MapBox.tsx | 355 | ||||
-rw-r--r-- | src/client/views/nodes/MapBox/MapBox2.tsx | 791 | ||||
-rw-r--r-- | src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx | 4 |
3 files changed, 1073 insertions, 77 deletions
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index 36be7d257..c44f5765a 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -1,6 +1,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Autocomplete, GoogleMap, GoogleMapProps, Marker } from '@react-google-maps/api'; +import { GoogleMapProps, Marker } from '@react-google-maps/api'; import BingMapsReact from 'bingmaps-react'; +import { EditableText } from 'browndash-components'; import { action, computed, IReactionDisposer, observable, ObservableMap, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -24,7 +25,7 @@ import { FieldView, FieldViewProps } from '../FieldView'; import { PinProps } from '../trails'; import './MapBox.scss'; import { MapBoxInfoWindow } from './MapBoxInfoWindow'; - +// amongus /** * MapBox architecture: * Main component: MapBox.tsx @@ -218,6 +219,27 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps controlDiv.appendChild(controlUI); return controlDiv; }; + /** + * Load and render all map markers + * @param marker + * @param place + */ + @action + private markerLoadHandler = (marker: google.maps.Marker, place: Doc) => { + place[Id] ? (this.markerMap[place[Id]] = marker) : null; + }; + + /** + * on clicking the map marker, set the selected place to the marker document & set infowindowopen to be true + * @param e + * @param place + */ + @action + private markerClickHandler = (e: google.maps.MapMouseEvent, place: Doc) => { + // set which place was clicked + this.selectedPlace = place; + // place.infoWindowOpen = true; + }; /** * Place the marker on google maps & store the empty marker as a MapMarker Document in allMarkers list @@ -298,25 +320,49 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; /** - * Load and render all map markers - * @param marker - * @param place + * function that reads the place inputed from searchbox, then zoom in on the location that's been autocompleted; + * add a customized temporary marker on the map */ @action - private markerLoadHandler = (marker: google.maps.Marker, place: Doc) => { - place[Id] ? (this.markerMap[place[Id]] = marker) : null; - }; + private handlePlaceChanged = () => { + const place = this.searchBox.getPlace(); - /** - * on clicking the map marker, set the selected place to the marker document & set infowindowopen to be true - * @param e - * @param place - */ - @action - private markerClickHandler = (e: google.maps.MapMouseEvent, place: Doc) => { - // set which place was clicked - this.selectedPlace = place; - place.infoWindowOpen = true; + if (!place.geometry || !place.geometry.location) { + // user entered the name of a place that wasn't suggested & pressed the enter key, or place details request failed + window.alert("No details available for input: '" + place.name + "'"); + return; + } + + // zoom in on the location of the search result + if (place.geometry.viewport) { + this._map.fitBounds(place.geometry.viewport); + } else { + this._map.setCenter(place.geometry.location); + this._map.setZoom(17); + } + + // customize icon => customized icon for the nature of the location selected + const icon = { + url: place.icon as string, + size: new google.maps.Size(71, 71), + origin: new google.maps.Point(0, 0), + anchor: new google.maps.Point(17, 34), + scaledSize: new google.maps.Size(25, 25), + }; + + // put temporary cutomized marker on searched location + this.searchMarkers.forEach(marker => { + marker.setMap(null); + }); + this.searchMarkers = []; + this.searchMarkers.push( + new window.google.maps.Marker({ + map: this._map, + icon, + title: place.name, + position: place.geometry.location, + }) + ); }; /** @@ -397,52 +443,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } /** - * function that reads the place inputed from searchbox, then zoom in on the location that's been autocompleted; - * add a customized temporary marker on the map - */ - @action - private handlePlaceChanged = () => { - const place = this.searchBox.getPlace(); - - if (!place.geometry || !place.geometry.location) { - // user entered the name of a place that wasn't suggested & pressed the enter key, or place details request failed - window.alert("No details available for input: '" + place.name + "'"); - return; - } - - // zoom in on the location of the search result - if (place.geometry.viewport) { - this._map.fitBounds(place.geometry.viewport); - } else { - this._map.setCenter(place.geometry.location); - this._map.setZoom(17); - } - - // customize icon => customized icon for the nature of the location selected - const icon = { - url: place.icon as string, - size: new google.maps.Size(71, 71), - origin: new google.maps.Point(0, 0), - anchor: new google.maps.Point(17, 34), - scaledSize: new google.maps.Size(25, 25), - }; - - // put temporary cutomized marker on searched location - this.searchMarkers.forEach(marker => { - marker.setMap(null); - }); - this.searchMarkers = []; - this.searchMarkers.push( - new window.google.maps.Marker({ - map: this._map, - icon, - title: place.name, - position: place.geometry.location, - }) - ); - }; - - /** * Handles toggle of sidebar on click the little comment button */ @computed get sidebarHandle() { @@ -591,13 +591,204 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }); }; + /** + * + * + * ERIC'S BING MAP CODE BELOW + * + * + * + **/ + + bingSearchBarContents: any = 'Boston, MA'; // For Bing Maps: The contents of the Bing search bar (string) + + geoDataRequestOptions = { + entityType: 'PopulatedPlace', + }; + + @action + updateView = () => { + this._bingMap.current.setView({ + center: new this.MicrosoftMaps.Location(this.dataDoc.latitude, this.dataDoc.longitude), + // zoom: location + }); + }; + + @action + createPushpin = (latitude: number, longitude: number) => { + // Stores the pushpin as a MapMarkerDocument + const mapMarker = Docs.Create.MapMarkerDocument(NumCast(latitude), NumCast(longitude), false, [], {}); + this.addDocument(mapMarker, this.annotationKey); + mapMarker.infoWindowOpen = true; + }; + + infobox: any; + pushpinClicked = (e: { target: { metadata: { title: any; description: any }; getLocation: () => any } }) => { + //Make sure the infobox has metadata to display. + if (e.target.metadata) { + //Set the infobox options with the metadata of the pushpin. // HOW DO I GET THE CORRECT INFOBOX FOR THIS PIN? CAN I use this e? + this.infobox.setOptions({ + location: e.target.getLocation(), + title: e.target.metadata.title, + description: e.target.metadata.description, + visible: true, + }); + } + }; + + //PushpinClicked using MapBoxInfoWindow + private pushpinClicked2 = (e: { target: { metadata: { title: any; description: any }; getLocation: () => any } }, pin: Doc) => { + // // set which place was clicked + // this.selectedPlace = place; + pin.infoWindowOpen = true; + // console.log("later:" + pin.infoWindowOpen) + }; + + /** + * Returns a list of MapMarkerDocument + */ + @computed get allMapPushpins() { + return DocListCast(this.dataDoc[this.annotationKey]); + } + /** + * Map OnClick create pushpin + */ + @action + mapOnClick = (e: { location: { latitude: any; longitude: any } }) => { + if (this.placePinOn) { + this.createPushpin(e.location.latitude, e.location.longitude); + this.addAllPins(); + this.placePinOn = false; + console.log('PUSHPIN CREATED'); + } + }; + + searched_pin: any; + /* + * For Bing Maps + * Called by search button's onClick + * Finds the geocode of the searched contents and sets location to that location + **/ + @action + bingSearch = async () => { + this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'click', this.mapOnClick); + this.addAllPins(); + + const location = await this.bingGeocode(this._bingMap, this.bingSearchBarContents); + + this.dataDoc.latitude = location.latitude; + this.dataDoc.longitude = location.longitude; + this.updateView(); + + // Creates a temporary pin but does not add it to the dataDoc + var temp = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(location.latitude, location.longitude), { + // title: this.bingSearchBarContents, + // subTitle: 'subtitle here', + // text: '1', + color: 'blue', + }); + if (temp != this.searched_pin || this.searched_pin == null) { + this._bingMap.current.entities.push(this.searched_pin); + this.searched_pin = temp; + } + + // console.log(this.allMapPushpins + // .filter(marker => marker.infoWindowOpen) + // .length) + // console.log(this.allMapPushpins + // .map(marker => console.log(marker.infoWindowOpen))) + }; + + /** + * Adds all pushpins in dataDoc onto the map + */ + @action + addAllPins = () => { + this._bingMap.current.entities.clear(); + //TODO: clear all infoboxes + if (this.searched_pin) this._bingMap.current.entities.push(this.searched_pin); + this.allMapPushpins.map(pin => this.createInfobox(pin)); + this.allMapPushpins.map(pin => this._bingMap.current.entities.push(pin)); + }; + + @action + createInfobox = (pin: any) => { + var pushPin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(pin.lat, pin.lng), { + // title: this.bingSearchBarContents, + // subTitle: 'subtitle here', + // text: '1' + }); + this._bingMap.current.entities.push(pushPin); + + var id = Utils.GenerateGuid(); + document.getElementById(id); + var infoboxTemplate = '<div class="customInfobox"><div class="title">{title}</div>{description}</div>'; + // var infowindowtemp = '<MapBoxInfoWindow key={Docs.Create.MapMarkerDocument(NumCast(40), NumCast(40), false, [], {})[Id]}{...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} place={Docs.Create.MapMarkerDocument(NumCast(40), NumCast(40), false, [], {})} markerMap={this.markerMap} PanelWidth={this.infoWidth} PanelHeight={this.infoHeight} moveDocument={this.moveDocument} isAnyChildContentActive={this.isAnyChildContentActive} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}/>' + + // console.log(<div id={id} style={{width:100, height:100, background:"blue"}}/>) + // var htmlString = ReactDOMServer.renderToString(<div id={id} style={{width:100, height:100, background:"blue"}}/>); + + //Create an infobox at the pin + this.infobox = new this.MicrosoftMaps.Infobox(new this.MicrosoftMaps.Location(pin.lat, pin.lng), { + htmlContent: infoboxTemplate, // style="width:100; height:100; background:blue" + visible: false, + }); + try { + // console.log(document.getElementById(id)) + // console.log(this.infobox) + // const root = ReactDOM.createRoot(document.getElementById(id)!); + // root.render(<div style={{width:100, height:100, background:"blue"}}/>); + } catch (e) { + console.log(e); + } + + //Assign the infobox to a map instance. + this.infobox.setMap(this._bingMap.current); + //Store some metadata with the pushpin. + pushPin.metadata = { + title: pushPin.title, + description: 'Pin description', + }; + // pushPin.infoWindowOpen = true; + + //Add a click event handler to the pushpin. + // For bing maps infobox + // this.MicrosoftMaps.Events.addHandler(pushPin, 'click', this.pushpinClicked); + + // For our infowindow + this.MicrosoftMaps.Events.addHandler(pushPin, 'click', (e: any) => this.pushpinClicked(e)); + // this.MicrosoftMaps.Events.addHandler(pushPin, 'mouseover', (e:any) => this.onHover(e)); + }; + + /** + * View options for bing maps + */ bingViewOptions = { - center: { latitude: defaultCenter.lat, longitude: defaultCenter.lng }, + 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, + }; + + /** + * Toggles mode for placing a pin down on map + */ + @observable + placePinOn: boolean | undefined; + @action + togglePlacePin = () => { + if (this.placePinOn) this.placePinOn = false; + else this.placePinOn = true; }; + bingMapReady = (map: any) => (this._bingMap = map.map); render() { const renderAnnotations = (docFilters?: () => string[]) => null; @@ -622,17 +813,18 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps {SnappingManager.GetIsDragging() ? null : renderAnnotations()} {this.annotationLayer} - {!MapBox.UseBing ? null : <BingMapsReact onMapReady={this.bingMapReady} bingMapsKey={bingApiKey} height="100%" mapOptions={this.bingMapOptions} width="100%" viewOptions={this.bingViewOptions} />} - <div style={{ display: MapBox.UseBing ? 'none' : undefined }}> - <GoogleMap mapContainerStyle={mapContainerStyle} onZoomChanged={this.zoomChanged} onCenterChanged={this.centered} onLoad={this.loadHandler} options={mapOptions}> - <Autocomplete onLoad={this.setSearchBox} onPlaceChanged={this.handlePlaceChanged}> - <input className="mapBox-input" ref={this.inputRef} type="text" onKeyDown={e => e.stopPropagation()} placeholder="Enter location" /> - </Autocomplete> + {!MapBox.UseBing ? null : <EditableText editing onEdit={(newText: string) => (this.bingSearchBarContents = newText)} placeholder="..." size="medium" text="Boston, MA" onKeyPress={(e: { key: any }) => console.log(e.key)} />} + + {!MapBox.UseBing ? null : <input type="button" value="Search" onClick={this.bingSearch} />} + + {!MapBox.UseBing ? null : this.placePinOn ? <input type="button" value="Place Pin Mode On" onClick={this.togglePlacePin} /> : <input type="button" value="Place Pin Mode Off" onClick={this.togglePlacePin} />} - {this.renderMarkers()} - {this.allMapMarkers + {!MapBox.UseBing ? null : ( + <BingMapsReact onMapReady={this.bingMapReady} bingMapsKey={bingApiKey} height="100%" mapOptions={this.bingMapOptions} width="100%" viewOptions={this.bingViewOptions}> + {this.allMapPushpins .filter(marker => marker.infoWindowOpen) .map(marker => ( + // console.log('this is a marker window') <MapBoxInfoWindow key={marker[Id]} {...this.props} @@ -646,9 +838,20 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} /> ))} - {/* {this.handleMapCenter(this._map)} */} - </GoogleMap> - </div> + </BingMapsReact> + )} + {/* <MapBoxInfoWindow + key={Docs.Create.MapMarkerDocument(NumCast(40), NumCast(40), false, [], {})[Id]} + {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} + place={Docs.Create.MapMarkerDocument(NumCast(40), NumCast(40), false, [], {})} + markerMap={this.markerMap} + PanelWidth={this.infoWidth} + PanelHeight={this.infoHeight} + moveDocument={this.moveDocument} + isAnyChildContentActive={this.isAnyChildContentActive} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + /> */} + {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : ( <MarqueeAnnotator rootDoc={this.rootDoc} diff --git a/src/client/views/nodes/MapBox/MapBox2.tsx b/src/client/views/nodes/MapBox/MapBox2.tsx new file mode 100644 index 000000000..4c28d4df1 --- /dev/null +++ b/src/client/views/nodes/MapBox/MapBox2.tsx @@ -0,0 +1,791 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Autocomplete, GoogleMap, GoogleMapProps, Marker } from '@react-google-maps/api'; +import BingMapsReact from 'bingmaps-react'; +import { EditableText } from 'browndash-components'; +import { action, computed, IReactionDisposer, observable, ObservableMap, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Doc, DocListCast, Opt, WidthSym } from '../../../../fields/Doc'; +import { Id } from '../../../../fields/FieldSymbols'; +import { InkTool } from '../../../../fields/InkField'; +import { NumCast, StrCast } from '../../../../fields/Types'; +import { emptyFunction, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils'; +import { Docs } from '../../../documents/Documents'; +import { DragManager } from '../../../util/DragManager'; +import { SnappingManager } from '../../../util/SnappingManager'; +import { UndoManager } from '../../../util/UndoManager'; +import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm'; +import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../../DocComponent'; +import { Colors } from '../../global/globalEnums'; +import { MarqueeAnnotator } from '../../MarqueeAnnotator'; +import { AnchorMenu } from '../../pdf/AnchorMenu'; +import { Annotation } from '../../pdf/Annotation'; +import { SidebarAnnos } from '../../SidebarAnnos'; +import { FieldView, FieldViewProps } from '../FieldView'; +import { PinProps } from '../trails'; +import './MapBox.scss'; +import { MapBoxInfoWindow } from './MapBoxInfoWindow'; + +/** + * 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 _global = (window /* browser */ || global /* node */) as any; + +const mapContainerStyle = { + height: '100%', +}; + +const defaultCenter = { + lat: 42.360081, + lng: -71.058884, +}; + +const mapOptions = { + fullscreenControl: false, +}; + +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 apiKey = process.env.GOOGLE_MAPS; + +const script = document.createElement('script'); +script.defer = true; +script.async = true; +script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places,drawing`; +console.log(script.src); +document.head.appendChild(script); + +/** + * 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, +// ], +// }, +// }); + +// options for searchbox in Google Maps Places Autocomplete API +const options = { + fields: ['formatted_address', 'geometry', 'name'], // note: level of details is charged by item per retrieval, not recommended to return all fields + strictBounds: false, + types: ['establishment'], // type pf places, subject of change according to user need +} as google.maps.places.AutocompleteOptions; + +@observer +export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps & Partial<GoogleMapProps>>() { + static UseBing = true; + private _dropDisposer?: DragManager.DragDropDisposer; + private _disposers: { [name: string]: IReactionDisposer } = {}; + private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); + @observable private _overlayAnnoInfo: Opt<Doc>; + showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno)); + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(MapBox2, fieldKey); + } + public get SidebarKey() { + return this.fieldKey + '-sidebar'; + } + private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void); + @computed get inlineTextAnnotations() { + return this.allMapMarkers.filter(a => a.textInlineAnnotations); + } + + @observable private _map: google.maps.Map = null as unknown as google.maps.Map; + @observable private selectedPlace: Doc | undefined; + @observable private markerMap: { [id: string]: google.maps.Marker } = {}; + @observable private center = navigator.geolocation ? navigator.geolocation.getCurrentPosition : defaultCenter; + @observable private _marqueeing: number[] | undefined; + @observable private _isAnnotating = false; + @observable private inputRef = React.createRef<HTMLInputElement>(); + @observable private searchMarkers: google.maps.Marker[] = []; + @observable private searchBox = new window.google.maps.places.Autocomplete(this.inputRef.current!, options); + @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); + @computed get allSidebarDocs() { + return DocListCast(this.dataDoc[this.SidebarKey]); + } + @computed get allMapMarkers() { + return DocListCast(this.dataDoc[this.annotationKey]); + } + @observable private toggleAddMarker = false; + private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); + + @observable _showSidebar = false; + @computed get SidebarShown() { + return this._showSidebar || this.layoutDoc._showSidebar ? true : false; + } + + static _canAnnotate = true; + static _hadSelection: boolean = false; + private _sidebarRef = React.createRef<SidebarAnnos>(); + private _ref: React.RefObject<HTMLDivElement> = React.createRef(); + + componentDidMount() { + this.props.setContentView?.(this); + } + + @action + private setSearchBox = (searchBox: any) => { + this.searchBox = searchBox; + }; + + // iterate allMarkers to size, center, and zoom map to contain all markers + private fitBounds = (map: google.maps.Map) => { + const curBounds = map.getBounds() ?? new window.google.maps.LatLngBounds(); + const isFitting = this.allMapMarkers.reduce((fits, place) => fits && curBounds?.contains({ lat: NumCast(place.lat), lng: NumCast(place.lng) }), true as boolean); + !isFitting && map.fitBounds(this.allMapMarkers.reduce((bounds, place) => bounds.extend({ lat: NumCast(place.lat), lng: NumCast(place.lng) }), new window.google.maps.LatLngBounds())); + }; + + /** + * Custom control for add marker button + * @param controlDiv + * @param map + */ + private CenterControl = () => { + const controlDiv = document.createElement('div'); + controlDiv.className = 'mapBox-addMarker'; + // Set CSS for the control border. + const controlUI = document.createElement('div'); + controlUI.style.backgroundColor = '#fff'; + controlUI.style.borderRadius = '3px'; + controlUI.style.cursor = 'pointer'; + controlUI.style.marginTop = '10px'; + controlUI.style.borderRadius = '4px'; + controlUI.style.marginBottom = '22px'; + controlUI.style.textAlign = 'center'; + controlUI.style.position = 'absolute'; + controlUI.style.width = '32px'; + controlUI.style.height = '32px'; + controlUI.title = 'Click to toggle marker mode. In marker mode, click on map to place a marker.'; + + const plIcon = document.createElement('img'); + plIcon.src = 'https://cdn4.iconfinder.com/data/icons/wirecons-free-vector-icons/32/add-256.png'; + plIcon.style.color = 'rgb(25,25,25)'; + plIcon.style.fontFamily = 'Roboto,Arial,sans-serif'; + plIcon.style.fontSize = '16px'; + plIcon.style.lineHeight = '32px'; + plIcon.style.left = '18'; + plIcon.style.top = '15'; + plIcon.style.position = 'absolute'; + plIcon.width = 14; + plIcon.height = 14; + plIcon.innerHTML = 'Add'; + controlUI.appendChild(plIcon); + + // Set CSS for the control interior. + const markerIcon = document.createElement('img'); + markerIcon.src = 'https://cdn0.iconfinder.com/data/icons/small-n-flat/24/678111-map-marker-1024.png'; + markerIcon.style.color = 'rgb(25,25,25)'; + markerIcon.style.fontFamily = 'Roboto,Arial,sans-serif'; + markerIcon.style.fontSize = '16px'; + markerIcon.style.lineHeight = '32px'; + markerIcon.style.left = '-2'; + markerIcon.style.top = '1'; + markerIcon.width = 30; + markerIcon.height = 30; + markerIcon.style.position = 'absolute'; + markerIcon.innerHTML = 'Add'; + controlUI.appendChild(markerIcon); + + // Setup the click event listeners + controlUI.addEventListener('click', () => { + if (this.toggleAddMarker === true) { + this.toggleAddMarker = false; + console.log('add marker button status:' + this.toggleAddMarker); + controlUI.style.backgroundColor = '#fff'; + markerIcon.style.color = 'rgb(25,25,25)'; + } else { + this.toggleAddMarker = true; + console.log('add marker button status:' + this.toggleAddMarker); + controlUI.style.backgroundColor = '#4476f7'; + markerIcon.style.color = 'rgb(255,255,255)'; + } + }); + controlDiv.appendChild(controlUI); + return controlDiv; + }; + + /** + * Place the marker on google maps & store the empty marker as a MapMarker Document in allMarkers list + * @param position - the LatLng position where the marker is placed + * @param map + */ + @action + private placeMarker = (position: google.maps.LatLng, map: google.maps.Map) => { + const marker = new google.maps.Marker({ + position: position, + map: map, + }); + map.panTo(position); + const mapMarker = Docs.Create.MapMarkerDocument(NumCast(position.lat()), NumCast(position.lng()), false, [], {}); + this.addDocument(mapMarker, this.annotationKey); + }; + + _loadPending = true; + /** + * store a reference to google map instance + * setup the drawing manager on the top right corner of map + * fit map bounds to contain all markers + * @param map + */ + @action + private loadHandler = (map: google.maps.Map) => { + this._map = map; + this._loadPending = true; + const centerControlDiv = this.CenterControl(); + map.controls[google.maps.ControlPosition.TOP_RIGHT].push(centerControlDiv); + //drawingManager.setMap(map); + // if (navigator.geolocation) { + // navigator.geolocation.getCurrentPosition( + // (position: Position) => { + // const pos = { + // lat: position.coords.latitude, + // lng: position.coords.longitude, + // }; + // this._map.setCenter(pos); + // } + // ); + // } else { + // alert("Your geolocation is not supported by browser.") + // }; + map.setZoom(NumCast(this.dataDoc.mapZoom, 2.5)); + map.setCenter(new google.maps.LatLng(NumCast(this.dataDoc.mapLat), NumCast(this.dataDoc.mapLng))); + setTimeout(() => { + if (this._loadPending && this._map.getBounds()) { + this._loadPending = false; + this.layoutDoc.fitContentsToBox && this.fitBounds(this._map); + } + }, 250); + // listener to addmarker event + this._map.addListener('click', (e: MouseEvent) => { + if (this.toggleAddMarker === true) { + this.placeMarker((e as any).latLng, map); + } + }); + }; + + @action + centered = () => { + if (this._loadPending && this._map.getBounds()) { + this._loadPending = false; + this.layoutDoc.fitContentsToBox && this.fitBounds(this._map); + } + this.dataDoc.mapLat = this._map.getCenter()?.lat(); + this.dataDoc.mapLng = this._map.getCenter()?.lng(); + }; + + @action + zoomChanged = () => { + if (this._loadPending && this._map.getBounds()) { + this._loadPending = false; + this.layoutDoc.fitContentsToBox && this.fitBounds(this._map); + } + this.dataDoc.mapZoom = this._map.getZoom(); + }; + + /** + * Load and render all map markers + * @param marker + * @param place + */ + @action + private markerLoadHandler = (marker: google.maps.Marker, place: Doc) => { + place[Id] ? (this.markerMap[place[Id]] = marker) : null; + }; + + /** + * on clicking the map marker, set the selected place to the marker document & set infowindowopen to be true + * @param e + * @param place + */ + @action + private markerClickHandler = (e: google.maps.MapMouseEvent, place: Doc) => { + // set which place was clicked + this.selectedPlace = place; + place.infoWindowOpen = true; + }; + + /** + * Called when dragging documents into map sidebar or directly into infowindow; to create a map marker, ref to MapMarkerDocument in Documents.ts + * @param doc + * @param sidebarKey + * @returns + */ + sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { + console.log('print all sidebar Docs'); + if (!this.layoutDoc._showSidebar) this.toggleSidebar(); + const docs = doc instanceof Doc ? [doc] : doc; + docs.forEach(doc => { + if (doc.lat !== undefined && doc.lng !== undefined) { + const existingMarker = this.allMapMarkers.find(marker => marker.lat === doc.lat && marker.lng === doc.lng); + if (existingMarker) { + Doc.AddDocToList(existingMarker, 'data', doc); + } else { + const marker = Docs.Create.MapMarkerDocument(NumCast(doc.lat), NumCast(doc.lng), false, [doc], {}); + this.addDocument(marker, this.annotationKey); + } + } + }); //add to annotation list + + return this.addDocument(doc, sidebarKey); // add to sidebar list + }; + + /** + * Removing documents from the sidebar + * @param doc + * @param sidebarKey + * @returns + */ + sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => { + if (this.layoutDoc._showSidebar) this.toggleSidebar(); + const docs = doc instanceof Doc ? [doc] : doc; + return this.removeDocument(doc, sidebarKey); + }; + + /** + * Toggle sidebar onclick the tiny comment button on the top right corner + * @param e + */ + sidebarBtnDown = (e: React.PointerEvent) => { + setupMoveUpEvents( + this, + e, + (e, down, delta) => + runInAction(() => { + const localDelta = this.props + .ScreenToLocalTransform() + .scale(this.props.NativeDimScaling?.() || 1) + .transformDirection(delta[0], delta[1]); + const fullWidth = this.layoutDoc[WidthSym](); + const mapWidth = fullWidth - this.sidebarWidth(); + if (this.sidebarWidth() + localDelta[0] > 0) { + this._showSidebar = true; + this.layoutDoc._width = fullWidth + localDelta[0]; + this.layoutDoc._sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth + localDelta[0])).toString() + '%'; + } else { + this._showSidebar = false; + this.layoutDoc._width = mapWidth; + this.layoutDoc._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(); + @computed get sidebarWidthPercent() { + return StrCast(this.layoutDoc._sidebarWidthPercent, '0%'); + } + @computed get sidebarColor() { + return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + '-backgroundColor'], '#e4e4e4')); + } + + /** + * function that reads the place inputed from searchbox, then zoom in on the location that's been autocompleted; + * add a customized temporary marker on the map + */ + @action + private handlePlaceChanged = () => { + const place = this.searchBox.getPlace(); + + if (!place.geometry || !place.geometry.location) { + // user entered the name of a place that wasn't suggested & pressed the enter key, or place details request failed + window.alert("No details available for input: '" + place.name + "'"); + return; + } + + // zoom in on the location of the search result + if (place.geometry.viewport) { + this._map.fitBounds(place.geometry.viewport); + } else { + this._map.setCenter(place.geometry.location); + this._map.setZoom(17); + } + + // customize icon => customized icon for the nature of the location selected + const icon = { + url: place.icon as string, + size: new google.maps.Size(71, 71), + origin: new google.maps.Point(0, 0), + anchor: new google.maps.Point(17, 34), + scaledSize: new google.maps.Size(25, 25), + }; + + // put temporary cutomized marker on searched location + this.searchMarkers.forEach(marker => { + marker.setMap(null); + }); + this.searchMarkers = []; + this.searchMarkers.push( + new window.google.maps.Marker({ + map: this._map, + icon, + title: place.name, + position: place.geometry.location, + }) + ); + }; + + /** + * Handles toggle of sidebar on click the little comment button + */ + @computed get sidebarHandle() { + return ( + <div + className="mapBox-overlayButton-sidebar" + key="sidebar" + title="Toggle Sidebar" + style={{ + display: !this.props.isContentActive() ? 'none' : undefined, + top: StrCast(this.rootDoc._showTitle) === 'title' ? 20 : 5, + backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, + }} + onPointerDown={this.sidebarBtnDown}> + <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> + </div> + ); + } + + // TODO: Adding highlight box layer to Maps + @action + toggleSidebar = () => { + //1.2 * w * ? = .2 * w .2/1.2 + const prevWidth = this.sidebarWidth(); + this.layoutDoc._showSidebar = (this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, '0%') === '0%' ? `${(100 * 0.2) / 1.2}%` : '0%') !== '0%'; + this.layoutDoc._width = this.layoutDoc._showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth); + }; + + sidebarDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), true); + }; + sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { + const bounds = this._ref.current!.getBoundingClientRect(); + this.layoutDoc._sidebarWidthPercent = '' + 100 * Math.max(0, 1 - (e.clientX - bounds.left) / bounds.width) + '%'; + this.layoutDoc._showSidebar = this.layoutDoc._sidebarWidthPercent !== '0%'; + e.preventDefault(); + return false; + }; + + setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => (this._setPreviewCursor = func); + + @action + onMarqueeDown = (e: React.PointerEvent) => { + if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { + setupMoveUpEvents( + this, + e, + action(e => { + MarqueeAnnotator.clearAnnotations(this._savedAnnotations); + this._marqueeing = [e.clientX, e.clientY]; + return true; + }), + returnFalse, + () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), + false + ); + } + }; + @action finishMarquee = (x?: number, y?: number) => { + this._marqueeing = undefined; + this._isAnnotating = false; + x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false); + }; + + addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => { + return this.addDocument(doc, annotationKey); + }; + + pointerEvents = () => { + return this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none'; + }; + @computed get annotationLayer() { + return ( + <div className="mapBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> + {this.inlineTextAnnotations + .sort((a, b) => NumCast(a.y) - NumCast(b.y)) + .map(anno => ( + <Annotation key={`${anno[Id]}-annotation`} {...this.props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} /> + ))} + </div> + ); + } + + getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => AnchorMenu.Instance?.GetAnchor(this._savedAnnotations, addAsAnnotation) ?? this.rootDoc; + + /** + * render contents in allMapMarkers (e.g. images with exifData) into google maps as map marker + * @returns + */ + private renderMarkers = () => { + return this.allMapMarkers.map(place => ( + <Marker key={place[Id]} position={{ lat: NumCast(place.lat), lng: NumCast(place.lng) }} onLoad={marker => this.markerLoadHandler(marker, place)} onClick={(e: google.maps.MapMouseEvent) => this.markerClickHandler(e, place)} /> + )); + }; + + // TODO: auto center on select a document in the sidebar + private handleMapCenter = (map: google.maps.Map) => { + // console.log("print the selected views in selectionManager:") + // if (SelectionManager.Views().lastElement()) { + // console.log(SelectionManager.Views().lastElement()); + // } + }; + + panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth(); + panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); + scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop)); + transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()]; + opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()]; + 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(), + }); + } + }); + }; + + + + + bingSearchBarContents: any = "Boston, MA"; // For Bing Maps: The contents of the Bing search bar (string) + + /* + * For Bing Maps + * Called by search button's onClick + * Finds the geocode of the searched contents and sets location to that location + **/ + @action + bingSearch = async() =>{ + + // clear all pins + this._bingMap.current.entities.clear(); + + const location = await this.bingGeocode(this._bingMap, this.bingSearchBarContents); + + // this.rootDoc.latitude =location.latitude; + // this.rootDoc.longitude =location.longitude; // TODO: How to update the rootDoc with the correct info? + //DocComponents file is where rootDoc is + + // call a helper method that updates the this._bingMap.current.setView, + // replaces this method call below + this._bingMap.current.setView({ + center: new this.MicrosoftMaps.Location(location.latitude, location.longitude), + // zoom: , + }); + + //Create custom Pushpin + var pin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(location.latitude, location.longitude), { + title: this.bingSearchBarContents, + subTitle: 'subtitle here', + text: '1' + }); + + //Add the pushpin to the map + this._bingMap.current.entities.push(pin); + // const mapMarker = Docs.Create.MapMarkerDocument(this._bingMap.location.latitude, this._bingMap.location.latitude, false, [], {}); + // this.addDocument(mapMarker, this.annotationKey); // tells mapbox to add this marker to set of annotations on doc + } + + + // /** + // * For Bing Maps + // * Place the marker on bing maps & store the empty marker as a MapMarker Document in allMarkers list + // * @param position - the LatLng position where the marker is placed + // * @param map + // */ + // @action + // private placeMarker = (position: google.maps.LatLng, map: google.maps.Map) => { + // const marker = new google.maps.Marker({ + // position: position, + // map: map, + // }); + // map.panTo(position); + // const mapMarker = Docs.Create.MapMarkerDocument(NumCast(position.lat()), NumCast(position.lng()), false, [], {}); + // this.addDocument(mapMarker, this.annotationKey); + // }; + + + + + + bingViewOptions = { + center: { latitude: defaultCenter.lat, longitude: defaultCenter.lng },// TODO: latitude: this.rootDoc.latitude, longitude: this.rootDoc.longitude + mapTypeId: 'grayscale', + }; + bingMapOptions = { + navigationBarMode: 'square', + }; + bingMapReady = (map: any) => (this._bingMap = map.map); + render() { + const renderAnnotations = (docFilters?: () => 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(); + // just a simple test of bing maps geocode api + // const loc = await this.bingGeocode(this._bingMap, 'Philadelphia, PA'); + // this._bingMap.current.setView({ + // mapTypeId: this.MicrosoftMaps.MapTypeId.aerial, + // center: new this.MicrosoftMaps.Location(loc.latitude, loc.longitude), + // zoom: 15, + // }); + }} + style={{ width: `calc(100% - ${this.sidebarWidthPercent})` }}> + + + <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div> + {renderAnnotations(this.opaqueFilter)} + {SnappingManager.GetIsDragging() ? null : renderAnnotations()} + {this.annotationLayer} + + + + {!MapBox2.UseBing ? null : + <EditableText editing + onEdit={(newText: string) => this.bingSearchBarContents = newText} + placeholder="..." + size="medium" + text="Boston, MA" + onKeyPress={e => console.log(e.key)} + />} + + {!MapBox2.UseBing ? null : + <input type="button" value="Search" + onClick={this.bingSearch}/>} + + + {!MapBox2.UseBing ? null : <BingMapsReact onMapReady={this.bingMapReady} bingMapsKey={bingApiKey} height="100%" mapOptions={this.bingMapOptions} width="100%" viewOptions={this.bingViewOptions} />} + {this.allMapMarkers + .filter(marker => marker.infoWindowOpen) + .map(marker => ( + <MapBoxInfoWindow + key={marker[Id]} + {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} + place={marker} + markerMap={this.markerMap} + PanelWidth={this.infoWidth} + PanelHeight={this.infoHeight} + moveDocument={this.moveDocument} + isAnyChildContentActive={this.isAnyChildContentActive} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + /> + ))} + + + <div style={{ display: MapBox2.UseBing ? 'none' : undefined }}> + <GoogleMap mapContainerStyle={mapContainerStyle} onZoomChanged={this.zoomChanged} onCenterChanged={this.centered} onLoad={this.loadHandler} options={mapOptions}> + <Autocomplete onLoad={this.setSearchBox} onPlaceChanged={this.handlePlaceChanged}> + <input className="mapBox-input" ref={this.inputRef} type="text" onKeyDown={e => e.stopPropagation()} placeholder="Enter location" /> + </Autocomplete> + + {this.renderMarkers()} + {this.allMapMarkers + .filter(marker => marker.infoWindowOpen) + .map(marker => ( + <MapBoxInfoWindow + key={marker[Id]} + {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} + place={marker} + markerMap={this.markerMap} + PanelWidth={this.infoWidth} + PanelHeight={this.infoHeight} + moveDocument={this.moveDocument} + isAnyChildContentActive={this.isAnyChildContentActive} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + /> + ))} + {/* {this.handleMapCenter(this._map)} */} + </GoogleMap> + </div> + + {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : ( + <MarqueeAnnotator + rootDoc={this.rootDoc} + anchorMenuClick={this.anchorMenuClick} + scrollTop={0} + down={this._marqueeing} + scaling={returnOne} + addDocument={this.addDocumentWrapper} + docView={this.props.docViewPath().lastElement()} + finishMarquee={this.finishMarquee} + savedAnnotations={this.savedAnnotations} + annotationLayer={this._annotationLayer.current} + selectionText={returnEmptyString} + mainCont={this._mainCont.current} + /> + )} + </div> + {/* </LoadScript > */} + <div className="mapBox-sidebar" style={{ position: 'absolute', right: 0, height: '100%', width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> + <SidebarAnnos + ref={this._sidebarRef} + {...this.props} + fieldKey={this.fieldKey} + rootDoc={this.rootDoc} + layoutDoc={this.layoutDoc} + dataDoc={this.dataDoc} + showSidebar={this.SidebarShown} + nativeWidth={NumCast(this.layoutDoc._nativeWidth)} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + PanelWidth={this.sidebarWidth} + sidebarAddDocument={this.sidebarAddDocument} + moveDocument={this.moveDocument} + removeDocument={this.sidebarRemoveDocument} + /> + </div> + {this.sidebarHandle} + </div> + ); + } +} diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx index 7b437c7de..0c1a62ddb 100644 --- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx +++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx @@ -47,7 +47,9 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi removeDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.RemoveDocFromList(this.props.place, 'data', d), true as boolean); render() { return ( - <InfoWindow anchor={this.props.markerMap[this.props.place[Id]]} onCloseClick={this.handleInfoWindowClose}> + <InfoWindow + // anchor={this.props.markerMap[this.props.place[Id]]} + onCloseClick={this.handleInfoWindowClose}> <div className="mapbox-infowindow"> <div style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }}> <CollectionStackingView |