From 8a537327fb685fa9267e25e550ec0aa7a3ae86cc Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Tue, 14 Apr 2020 04:01:24 -0700
Subject: added bidirectional geocoding for map view

---
 src/client/views/collections/CollectionMapView.tsx | 128 +++++++++++++++------
 1 file changed, 94 insertions(+), 34 deletions(-)

(limited to 'src/client/views/collections/CollectionMapView.tsx')

diff --git a/src/client/views/collections/CollectionMapView.tsx b/src/client/views/collections/CollectionMapView.tsx
index a2d3c328d..8cdc145b8 100644
--- a/src/client/views/collections/CollectionMapView.tsx
+++ b/src/client/views/collections/CollectionMapView.tsx
@@ -4,37 +4,44 @@ import { Doc, Opt, DocListCast } from "../../../new_fields/Doc";
 import { documentSchema } from "../../../new_fields/documentSchemas";
 import { Id } from "../../../new_fields/FieldSymbols";
 import { makeInterface } from "../../../new_fields/Schema";
-import { Cast, NumCast, ScriptCast, StrCast } from "../../../new_fields/Types";
+import { Cast, NumCast, ScriptCast, StrCast, BoolCast } from "../../../new_fields/Types";
 import { TraceMobx } from "../../../new_fields/util";
 import "./CollectionMapView.scss";
 import { CollectionSubView } from "./CollectionSubView";
 import React = require("react");
 import { DocumentManager } from "../../util/DocumentManager";
 import { UndoManager } from "../../util/UndoManager";
+import { IReactionDisposer, reaction } from "mobx";
+import requestPromise = require("request-promise");
 
 type MapSchema = makeInterface<[typeof documentSchema]>;
 const MapSchema = makeInterface(documentSchema);
 
-export type LocationData = google.maps.LatLngLiteral & { address?: string };
+export type LocationData = google.maps.LatLngLiteral & {
+    address?: string
+    resolvedAddress?: string;
+    zoom?: number;
+};
+
+const base = "https://maps.googleapis.com/maps/api/geocode/json?";
 
 @observer
 class CollectionMapView extends CollectionSubView<MapSchema, Partial<MapProps> & { google: any }>(MapSchema) {
 
-    getLocation = (doc: Opt<Doc>, fieldKey: string) => {
+    private mapRef = React.createRef<Map>();
+    private addressUpdaters: IReactionDisposer[] = [];
+    private latlngUpdaters: IReactionDisposer[] = [];
+
+    getLocation = (doc: Opt<Doc>, fieldKey: string): Opt<LocationData> => {
         if (doc) {
-            let lat: Opt<number> = Cast(doc[fieldKey + "-lat"], "number", null);
-            let lng: Opt<number> = Cast(doc[fieldKey + "-lng"], "number", null);
+            const lat: Opt<number> = Cast(doc[fieldKey + "-lat"], "number", null);
+            const lng: Opt<number> = Cast(doc[fieldKey + "-lng"], "number", null);
             const zoom: Opt<number> = Cast(doc[fieldKey + "-zoom"], "number", null);
-            const address = Cast(doc[fieldKey + "-address"], "string", null);
-            if (address) {
-                // use geo service to convert to lat/lng
-                lat = lat;
-                lng = lng;
-            }
             return lat !== undefined && lng !== undefined ? ({ lat, lng, zoom }) : undefined;
         }
         return undefined;
     }
+
     renderMarker(layout: Doc, icon: Opt<google.maps.Icon>) {
         const location = this.getLocation(layout, "mapLocation");
         return !location ? (null) :
@@ -43,6 +50,7 @@ class CollectionMapView extends CollectionSubView<MapSchema, Partial<MapProps> &
                 label={StrCast(layout.title)}
                 position={{ lat: location.lat, lng: location.lng }}
                 onClick={async () => {
+                    this.map.panTo(location);
                     this.layoutDoc[this.props.fieldKey + "-mapCenter-lat"] = 0;
                     this.layoutDoc[this.props.fieldKey + "-mapCenter-lat"] = location.lat;
                     this.layoutDoc[this.props.fieldKey + "-mapCenter-lng"] = location.lng;
@@ -60,6 +68,73 @@ class CollectionMapView extends CollectionSubView<MapSchema, Partial<MapProps> &
                 icon={icon}
             />;
     }
+
+    private get contents() {
+        this.addressUpdaters.forEach(disposer => disposer());
+        this.addressUpdaters = [];
+        this.latlngUpdaters.forEach(disposer => disposer());
+        this.latlngUpdaters = [];
+        return this.childLayoutPairs.map(({ layout }) => {
+            let icon: Opt<google.maps.Icon>, iconUrl: Opt<string>;
+            if ((iconUrl = StrCast(this.props.Document.mapIconUrl, null))) {
+                const iconWidth = NumCast(layout["mapLocation-iconWidth"], 45);
+                const iconHeight = NumCast(layout["mapLocation-iconHeight"], 45);
+                const iconSize = new google.maps.Size(iconWidth, iconHeight);
+                icon = {
+                    size: iconSize,
+                    scaledSize: iconSize,
+                    url: iconUrl
+                };
+            }
+            this.addressUpdaters.push(reaction(
+                () => ({
+                    lat: NumCast(layout["mapLocation-lat"]),
+                    lng: NumCast(layout["mapLocation-lng"])
+                }),
+                ({ lat, lng }) => {
+                    if (!BoolCast(layout._ignoreNextUpdate)) {
+                        if (lat !== undefined && lng !== undefined) {
+                            const target = `${base}latlng=${lat},${lng}&key=${process.env.GOOGLE_MAPS_GEO!}`;
+                            requestPromise.get(target).then(res => {
+                                layout._ignoreNextUpdate = true;
+                                layout["mapLocation-address"] = JSON.parse(res).results[0]?.formatted_address || "<invalid address>";
+                            });
+                        }
+                    } else {
+                        layout._ignoreNextUpdate = false;
+                    }
+                }
+            ));
+            this.latlngUpdaters.push(reaction(
+                () => ({ address: Cast(layout["mapLocation-address"], "string", null) }),
+                ({ address }) => {
+                    if (!BoolCast(layout._ignoreNextUpdate)) {
+                        if (address && address.length) {
+                            const target = `${base}address=${address.replace(/\s+/g, "+")}&key=${process.env.GOOGLE_MAPS_GEO!}`;
+                            requestPromise.get(target).then(res => {
+                                const result = JSON.parse(res).results[0];
+                                const { lat, lng } = result.geometry.location;
+                                layout._ignoreNextUpdate = true;
+                                layout["mapLocation-lat"] = lat;
+                                layout._ignoreNextUpdate = true;
+                                layout["mapLocation-lng"] = lng;
+                                layout._ignoreNextUpdate = true;
+                                layout["mapLocation-address"] = result.formatted_address;
+                            });
+                        }
+                    } else {
+                        layout._ignoreNextUpdate = false;
+                    }
+                }
+            ));
+            return this.renderMarker(layout, icon);
+        });
+    }
+
+    private get map() {
+        return (this.mapRef.current as any).map;
+    }
+
     render() {
         const { childLayoutPairs } = this;
         const { Document } = this.props;
@@ -76,33 +151,18 @@ class CollectionMapView extends CollectionSubView<MapSchema, Partial<MapProps> &
             onWheel={e => e.stopPropagation()}
             onPointerDown={e => (e.button === 0 && !e.ctrlKey) && e.stopPropagation()} >
             <Map
+                ref={this.mapRef}
                 google={this.props.google}
                 zoom={center.zoom || 10}
                 initialCenter={center}
-                onBoundsChanged={(props, map, e) => console.log("ON_BOUNDS_CHANGED", props, map, e)}
-                onRecenter={(props, map, e) => console.log("ON_RECENTER", props, map, e)}
-                onDragend={(centerMoved, center) => console.log("ON_DRAGEND", centerMoved, center)}
-                onProjectionChanged={(props, map, e) => console.log("ON_PROJ_CHANGED", props, map, e)}
-                onCenterChanged={((props, map, e) => {
-                    console.log("ON_CENTER_CHANGED", props, map, e);
-                    Document[this.props.fieldKey + "-mapCenter-lat"] = typeof e?.center?.lat === "number" ? e.center.lat : center!.lat;
-                    Document[this.props.fieldKey + "-mapCenter-lng"] = typeof e?.center?.lng === "number" ? e.center.lng : center!.lng;
-                })}
+                center={center}
+                onDragend={() => {
+                    const { center } = this.map;
+                    Document[this.props.fieldKey + "-mapCenter-lat"] = center.lat();
+                    Document[this.props.fieldKey + "-mapCenter-lng"] = center.lng();
+                }}
             >
-                {childLayoutPairs.map(({ layout }) => {
-                    let icon: Opt<google.maps.Icon>, iconUrl: Opt<string>;
-                    if ((iconUrl = StrCast(Document.mapIconUrl, null))) {
-                        const iconWidth = NumCast(layout["mapLocation-iconWidth"], 45);
-                        const iconHeight = NumCast(layout["mapLocation-iconHeight"], 45);
-                        const iconSize = new google.maps.Size(iconWidth, iconHeight);
-                        icon = {
-                            size: iconSize,
-                            scaledSize: iconSize,
-                            url: iconUrl
-                        };
-                    }
-                    return this.renderMarker(layout, icon);
-                })}
+                {this.contents}
             </Map>
         </div>;
     }
-- 
cgit v1.2.3-70-g09d2

