aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections/MapView/CollectionMapView.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/collections/MapView/CollectionMapView.tsx')
-rw-r--r--src/client/views/collections/MapView/CollectionMapView.tsx272
1 files changed, 272 insertions, 0 deletions
diff --git a/src/client/views/collections/MapView/CollectionMapView.tsx b/src/client/views/collections/MapView/CollectionMapView.tsx
new file mode 100644
index 000000000..1166de61c
--- /dev/null
+++ b/src/client/views/collections/MapView/CollectionMapView.tsx
@@ -0,0 +1,272 @@
+import { GoogleMap, Marker, InfoWindow, InfoBox, useJsApiLoader, LoadScript, GoogleMapProps, StandaloneSearchBox, Autocomplete } from '@react-google-maps/api';
+import { observable, action, computed, Lambda, runInAction, IReactionDisposer } from "mobx";
+import { observer } from "mobx-react";
+import { Doc, DocListCast, Field, FieldResult, Opt } from "../../../../fields/Doc";
+import { documentSchema } from "../../../../fields/documentSchemas";
+import { Id } from "../../../../fields/FieldSymbols";
+import { makeInterface } from "../../../../fields/Schema";
+import { Cast, NumCast, ScriptCast, StrCast } from "../../../../fields/Types";
+import { LinkManager } from "../../../util/LinkManager";
+import { undoBatch, UndoManager } from "../../../util/UndoManager";
+import "./CollectionMapView.scss";
+import { CollectionSubView } from "../CollectionSubView";
+import React = require("react");
+import requestPromise = require("request-promise");
+import ReactDOM from 'react-dom';
+import { DragManager } from '../../../util/DragManager';
+import { MapMarker } from '../../nodes/MapBox/MapMarker';
+
+
+/**
+ * Idea behind storing a marker:
+ * 1. on the map api, have a button "add marker" that adds the marker on the map & store the marker as a node in the collection
+ * (but don't render the marker in the collection itself)
+ * 2. each marker, as a node, has the same feature as all other nodes for linking (the same way one could form a link between a node inside a child collection
+ * and a node outside the child collection)
+ *
+ * /util/LinkManager.ts -- link relations
+ *
+ */
+
+type MapSchema = makeInterface<[typeof documentSchema]>;
+const MapSchema = makeInterface(documentSchema);
+
+export type Coordinates = {
+ lat: number,
+ lng: number,
+}
+
+export type LocationData = {
+ id: string;
+ pos: Coordinates;
+};
+
+const mapContainerStyle = {
+ height: '100%',
+};
+
+const defaultCenter = {
+ lat: 38.685,
+ lng: -115.234,
+};
+
+const mapOptions = {
+ fullscreenControl: false,
+}
+
+const drawingManager = new 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,
+ ],
+ },
+});
+
+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 default class CollectionMapView extends CollectionSubView<MapSchema, Partial<GoogleMapProps>>(MapSchema) {
+ private _dropDisposer?: DragManager.DragDropDisposer;
+ private _disposers: { [name: string]: IReactionDisposer } = {};
+
+
+ @observable private _map = null as unknown as google.maps.Map;
+ @observable private selectedPlace: LocationData | undefined;
+ @observable private markerMap: { [id: string]: google.maps.Marker } = {};
+ @observable private center = defaultCenter;
+ @observable private zoom = 2.5;
+ @observable private infoWindowOpen = false;
+ @observable private bounds = new window.google.maps.LatLngBounds();
+ @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 myPlaces: LocationData[] = [
+ { id: "id1", pos: { lat: 39.09366509575983, lng: -94.58751660204751 } },
+ { id: "id2", pos: { lat: 41.82399, lng: -71.41283 } },
+ { id: "id3", pos: { lat: 47.606214, lng: -122.33207 } },
+ ];
+
+ @action
+ private setSearchBox = (searchBox: any) => {
+ this.searchBox = searchBox;
+ }
+
+ // iterate myPlaces to size, center, and zoom map to contain all markers
+ private fitBounds = (map: google.maps.Map) => {
+ console.log('map bound is:' + this.bounds);
+ this.myPlaces ? this.myPlaces.map(place => {
+ this.bounds.extend(place.pos!);
+ }) : null;
+ map.fitBounds(this.bounds);
+ }
+
+ // store a reference to google map instance; fit map bounds to contain all markers
+ @action
+ private loadHandler = (map: google.maps.Map) => {
+ this._map = map;
+ drawingManager.setMap(map);
+ this.fitBounds(map);
+ }
+
+ @action
+ private markerClickHandler = (e: MouseEvent, place: any) => {
+ // set which place was clicked
+ this.selectedPlace = place;
+
+ console.log(this.selectedPlace);
+
+ // used so clicking a second marker works
+ if (this.infoWindowOpen) {
+ this.infoWindowOpen = false;
+ console.log("closeinfowindow")
+ }
+ this.infoWindowOpen = true;
+ console.log("open infowindow")
+ }
+
+ @action
+ private handleInfoWindowClose = () => {
+ if (this.infoWindowOpen) {
+ this.infoWindowOpen = false;
+ }
+ this.infoWindowOpen = false;
+ this.selectedPlace = undefined;
+ }
+
+ @action
+ private handlePlaceChanged = () => {
+ console.log(this.searchBox);
+ 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) {
+ console.log(this._map);
+ this._map.fitBounds(place.geometry.viewport);
+ } else {
+ console.log(this._map);
+ 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,
+ })
+ )
+ }
+
+
+ @action
+ private addMarker = (location: google.maps.LatLng | undefined, map: google.maps.Map) => {
+ new window.google.maps.Marker({
+ position: location,
+ map: map
+ });
+ }
+
+ @action
+ private markerLoadHandler = (marker: google.maps.Marker, place: LocationData) => {
+ place.id ? this.markerMap[place.id] = marker : null;
+ }
+
+ render() {
+ const { Document, fieldKey, isContentActive: active } = this.props;
+
+ return <div className="collectionMapView" ref={this.createDashEventsTarget}>
+
+ <div className={"collectionMapView-contents"}
+ style={{ pointerEvents: active() ? undefined : "none", overflow: 'hidden' }}
+ onWheel={e => e.stopPropagation()}
+ onPointerDown={e => (e.button === 0 && !e.ctrlKey) && e.stopPropagation()} >
+ {/* <LoadScript
+ googleMapsApiKey={process.env.GOOGLE_MAPS!}
+ libraries={['places', 'drawing']}
+ > */}
+ <div className="map-wrapper">
+ <GoogleMap
+ mapContainerStyle={mapContainerStyle}
+ zoom={this.zoom}
+ center={this.center}
+ onLoad={map => this.loadHandler(map)}
+ options={mapOptions}
+ >
+ <Autocomplete
+ onLoad={this.setSearchBox}
+ onPlaceChanged={this.handlePlaceChanged}>
+ <input ref={this.inputRef} className="searchbox" type="text" placeholder="Search anywhere:" />
+ </Autocomplete>
+
+ {this.myPlaces ? this.myPlaces.map(place =>
+ <Marker
+ position={place.pos}
+ onLoad={marker => this.markerLoadHandler(marker, place)}
+ onClick={e => this.markerClickHandler(e, place)}
+ draggable={false}
+ />
+ ) : null}
+ {this.infoWindowOpen && this.selectedPlace && (
+ <InfoWindow
+ anchor={this.markerMap[this.selectedPlace.id!]}
+ onCloseClick={this.handleInfoWindowClose}
+ >
+ <div style={{ backgroundColor: 'white', opacity: 0.75, padding: 12 }}>
+ <div style={{ fontSize: 16 }}>
+ <div>
+ <img src="http://placekitten.com/200/300" />
+ <hr />
+ <form>
+ <label>Title: </label><br />
+ <input type="text" id="fname" name="fname"></input><br />
+ <label>Desription: </label><br />
+ <textarea style={{ height: 150 }} id="lname" name="lname" placeholder="Notes, a short description of this location, a brief comment, etc."></textarea>
+ </form>
+ <hr />
+ <div>
+ <button>New link+</button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </InfoWindow>
+ )}
+ </GoogleMap>
+ </div>
+ {/* </LoadScript > */}
+ </div >
+ </div >;
+ }
+
+} \ No newline at end of file