JS Mojo Geocoder Demo

Added a JS example that demos encapsulating Google's geocoder web service with a Mojo application.

BUG=
R=eseidel@chromium.org

Review URL: https://codereview.chromium.org/848213003
diff --git a/examples/js/maps/BUILD.gn b/examples/js/maps/BUILD.gn
new file mode 100644
index 0000000..d10a973
--- /dev/null
+++ b/examples/js/maps/BUILD.gn
@@ -0,0 +1,17 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//mojo/public/tools/bindings/mojom.gni")
+
+group("maps") {
+  deps = [
+    ":bindings",
+  ]
+}
+
+mojom("bindings") {
+  sources = [
+    "geocoder.mojom",
+  ]
+}
diff --git a/examples/js/maps/demo.js b/examples/js/maps/demo.js
new file mode 100644
index 0000000..aa6e783
--- /dev/null
+++ b/examples/js/maps/demo.js
@@ -0,0 +1,79 @@
+#!mojo mojo:js_content_handler
+// Demonstate a Mojo wrapper for the Geocoder JSON API. The application
+// connects to geocoder_service.js which implements geocoder.mojom.
+// To run this application with mojo_shell, set DIR to be the absolute path
+// for this directory, then:
+//   mojo_shell file://$DIR/demo.js
+
+define("main", [
+  "console",
+  "examples/js/maps/geocoder.mojom",
+  "mojo/public/js/core",
+  "mojo/public/js/unicode",
+  "mojo/services/public/js/application",
+  "third_party/js/url",
+], function(console, geocoder, core, unicode, application, url) {
+
+  const Application = application.Application;
+  const Geocoder = geocoder.Geocoder;
+  const Result = geocoder.Result;
+  const Location = geocoder.Location;
+  const Status = geocoder.Status;
+  const Options = geocoder.Options;
+  const URL = url.URL;
+
+  var geocoderService;
+
+  function demoAddressToLocation() {
+    console.log("Demo GeocoderServce.AddressToLocation()");
+    var addr = "1365 Shorebird way, Mountain View, CA";
+    geocoderService.addressToLocation(addr, new Options).then(
+      function(rv) {
+        if (rv.status == Status.OK) {
+          for (var i = 0; i < rv.results.length; i++) {
+            var address = rv.results[i].formatted_address;
+            var location = rv.results[i].geometry.location;
+            console.log("Latitude,longitude for \"" + address + "\":");
+            console.log(location.latitude + ", " + location.longitude);
+
+            console.log("");
+            demoLocationToAddress();
+          }
+        } else {
+          console.log("Geocoder request failed status=" + rv.status);
+        }
+      });
+  }
+
+  function demoLocationToAddress() {
+    console.log("Demo GeocoderServce.LocationToAddress()");
+    var coords = new Location({
+      latitude: 37.41811752319336,
+      longitude: -122.07335662841797,
+    });
+    geocoderService.locationToAddress(coords, new Options).then(
+      function(rv) {
+        if (rv.status == Status.OK) {
+          for (var i = 0; i < rv.results.length; i++) {
+            var address = rv.results[i].formatted_address;
+            var location = rv.results[i].geometry.location;
+            console.log("Latitude,longitude for \"" + address + "\":");
+            console.log(location.latitude + ", " + location.longitude);
+          }
+        } else {
+          console.log("Geocoder request failed status=" + rv.status);
+        }
+      });
+  }
+
+  class Demo extends Application {
+    initialize() {
+      var geocoderURL = new URL(this.url).resolve("geocoder_service.js");
+      geocoderService = this.shell.connectToService(geocoderURL, Geocoder);
+      demoAddressToLocation();
+    }
+  }
+
+  return Demo;
+});
+
diff --git a/examples/js/maps/geocoder.mojom b/examples/js/maps/geocoder.mojom
new file mode 100644
index 0000000..d7cbe4c
--- /dev/null
+++ b/examples/js/maps/geocoder.mojom
@@ -0,0 +1,68 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// All of the types that follow are simple mappings of the types defined by the
+// "Google Maps JavaScript API v3" defined here:
+// https://developers.google.com/maps/documentation/javascript/geocoding
+
+module geocoder;
+
+struct Location {
+  float latitude;
+  float longitude;
+};
+
+struct LocationType {
+  const string ROOFTOP = "ROOFTOP";
+  const string RANGE_INTERPOLATED = "RANGE_INTERPOLATED";
+  const string GEOMETRIC_CENTER = "GEOMETRIC_CENTER";
+  const string APPROXIMATE = "APPROXIMATE";
+};
+
+struct Bounds {
+  Location northeast;
+  Location southwest;
+};
+
+struct ComponentRestrictions {
+  string? administrative_area; 
+  string? country;
+  string? locality;
+  string? postal_code;
+  string? route;
+};
+
+struct Options {
+  ComponentRestrictions? restrictions;
+  Location? location;
+  string? region;
+};
+
+struct Geometry {
+  Location location;
+  LocationType location_type;
+  Bounds viewport;
+  Bounds? bounds;
+};
+
+struct Result {
+  bool partial_match;
+  Geometry geometry;
+  string formatted_address;
+  array<string> types;
+  // TBD address_components
+};
+
+struct Status {
+  const string OK = "OK";
+  const string ZERO_RESULTS = "ZERO_RESULTS";
+  const string OVER_QUERY_LIMIT = "OVER_QUERY_LIMIT";
+  const string REQUEST_DENIED = "REQUEST_DENIED";
+  const string INVALID_REQUEST = "INVALID_REQUEST";
+};
+
+interface Geocoder {
+  AddressToLocation(string address, Options? options) => (string status, array<Result>? results);
+  LocationToAddress(Location location, Options? options) => (string status, array<Result>? results);
+};
diff --git a/examples/js/maps/geocoder_service.js b/examples/js/maps/geocoder_service.js
new file mode 100644
index 0000000..351a460
--- /dev/null
+++ b/examples/js/maps/geocoder_service.js
@@ -0,0 +1,128 @@
+#!mojo mojo:js_content_handler
+
+define("main", [
+  "console",
+  "examples/js/maps/geocoder.mojom",
+  "mojo/public/js/core",
+  "mojo/public/js/unicode",
+  "mojo/services/public/js/application",
+  "mojo/services/network/public/interfaces/network_service.mojom",
+  "mojo/services/network/public/interfaces/url_loader.mojom",
+  "third_party/js/url",
+], function(console, geocoder, core, unicode, application, network, loader, url) {
+
+  const Application = application.Application;
+  const Bounds = geocoder.Bounds;
+  const Geocoder = geocoder.Geocoder;
+  const Geometry = geocoder.Geometry;
+  const Location = geocoder.Location;
+  const NetworkService = network.NetworkService;
+  const Result = geocoder.Result;
+  const Status = geocoder.Status;
+  const URLRequest = loader.URLRequest;
+  const URL = url.URL;
+
+  var netService;
+
+  Location.prototype.queryString = function() {
+    // TBD: format floats to 6 decimal places
+    return this.latitude + ", " + this.longitude;
+  }
+
+  Location.fromJSON = function(json) {
+    return !json ? null : new Location({
+      latitude: json.lat,
+      longitude: json.lng,
+    });
+  }
+
+  Bounds.fromJSON = function(json) {
+    return !json ? null : new Bounds({
+      northeast: Location.fromJSON(json.northeast),
+      southwest: Location.fromJSON(json.southwest),
+    });
+  }
+
+  Geometry.fromJSON = function(json) {
+    return !json ? null : new Geometry({
+      location: Location.fromJSON(json.location),
+      location_type: json.location_type,
+      viewport: Bounds.fromJSON(json.viewport),
+      bounds: Bounds.fromJSON(json.bounds),
+    });
+  }
+
+  Result.fromJSON = function(json) {
+    return !json ? null : new Result({
+      partial_match: !!json.partial_match,
+      formatted_address: json.formatted_address,
+      geometry: Geometry.fromJSON(json.geometry),
+      types: json.types,
+      // TBD: address_components
+    });
+  }
+
+  function parseGeocodeResponse(arrayBuffer) {
+    return JSON.parse(unicode.decodeUtf8String(new Uint8Array(arrayBuffer)));
+  }
+
+  function geocodeRequest(url) {
+    return new Promise(function(resolveRequest) {
+      var urlLoader;
+      netService.createURLLoader(function(urlLoaderProxy) {
+        urlLoader = urlLoaderProxy;
+      });
+
+      var urlRequest = new URLRequest({
+        url: url.format(),
+        method: "GET",
+        auto_follow_redirects: true
+      });
+
+      urlLoader.start(urlRequest).then(function(urlLoaderResult) {
+        core.drainData(urlLoaderResult.response.body).then(
+          function(drainDataResult) {
+            // TBD: handle drainData failure
+            var value = parseGeocodeResponse(drainDataResult.buffer);
+            resolveRequest({
+              status: value.status,
+              results: value.results.map(Result.fromJSON),
+            });
+          });
+      });
+    });
+  }
+
+  function geocodeURL(key, value, options) {
+    var url = new URL("https://maps.googleapis.com/maps/api/geocode/json");
+    url.query = {};
+    url.query[key] = value;
+    // TBD: add options url.query
+    return url;
+  }
+
+  class GeocoderImpl {
+    addressToLocation(address, options) {
+      return geocodeRequest(geocodeURL("address", address, options));
+    }
+
+    locationToAddress(location, options) {
+      return geocodeRequest(
+          geocodeURL("latlng", location.queryString(), options));
+    }
+  }
+
+  class GeocoderService extends Application {
+    initialize() {
+      netService = this.shell.connectToService(
+        "mojo:network_service", NetworkService);
+    }
+
+    acceptConnection(initiatorURL, serviceProvider) {
+      serviceProvider.provideService(Geocoder, GeocoderImpl);
+    }
+  }
+
+  return GeocoderService;
+});
+