diff options
author | Sam Wilkins <samwilkins333@gmail.com> | 2020-04-14 04:01:24 -0700 |
---|---|---|
committer | Sam Wilkins <samwilkins333@gmail.com> | 2020-04-14 04:01:24 -0700 |
commit | 8a537327fb685fa9267e25e550ec0aa7a3ae86cc (patch) | |
tree | 8a3ee4b3d0cb2bde144316d0b81eb55059985c60 | |
parent | c551680dd75504e979482ba42ae6ab2b76feb4c3 (diff) |
added bidirectional geocoding for map view
-rw-r--r-- | src/client/views/collections/CollectionMapView.tsx | 128 | ||||
-rw-r--r-- | webpack.config.js | 64 |
2 files changed, 126 insertions, 66 deletions
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>; } diff --git a/webpack.config.js b/webpack.config.js index 1027f29a6..6265883fd 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -20,7 +20,7 @@ const dotenv = require('dotenv'); function transferEnvironmentVariables() { const prefix = "_CLIENT_"; - const env = dotenv.config({ debug: true }).parsed; + const env = dotenv.config().parsed; if (env) { plugins.push(new webpack.DefinePlugin(Object.keys(env).reduce((mapping, envKey) => { if (envKey.startsWith(prefix)) { @@ -64,42 +64,42 @@ module.exports = { }, module: { rules: [{ - test: [/\.tsx?$/], - use: [{ - loader: 'ts-loader', - options: { - transpileOnly: true - } - }] - }, - { - test: /\.scss|css$/, - use: [{ - loader: "style-loader" + test: [/\.tsx?$/], + use: [{ + loader: 'ts-loader', + options: { + transpileOnly: true + } + }] }, { - loader: "css-loader" + test: /\.scss|css$/, + use: [{ + loader: "style-loader" + }, + { + loader: "css-loader" + }, + { + loader: "sass-loader" + } + ] }, { - loader: "sass-loader" + test: /\.(jpg|png|pdf)$/, + use: [{ + loader: 'file-loader' + }] + }, + { + test: /\.(png|jpg|gif)$/i, + use: [{ + loader: 'url-loader', + options: { + limit: 8192 + } + }] } - ] - }, - { - test: /\.(jpg|png|pdf)$/, - use: [{ - loader: 'file-loader' - }] - }, - { - test: /\.(png|jpg|gif)$/i, - use: [{ - loader: 'url-loader', - options: { - limit: 8192 - } - }] - } ] }, plugins, |