• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 *
3 * Copyright 2015 gRPC authors.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 */
18
19var PROTO_PATH = __dirname + '/../../../protos/route_guide.proto';
20
21var fs = require('fs');
22var parseArgs = require('minimist');
23var path = require('path');
24var _ = require('lodash');
25var grpc = require('grpc');
26var protoLoader = require('@grpc/proto-loader');
27var packageDefinition = protoLoader.loadSync(
28    PROTO_PATH,
29    {keepCase: true,
30     longs: String,
31     enums: String,
32     defaults: true,
33     oneofs: true
34    });
35var routeguide = grpc.loadPackageDefinition(packageDefinition).routeguide;
36
37var COORD_FACTOR = 1e7;
38
39/**
40 * For simplicity, a point is a record type that looks like
41 * {latitude: number, longitude: number}, and a feature is a record type that
42 * looks like {name: string, location: point}. feature objects with name===''
43 * are points with no feature.
44 */
45
46/**
47 * List of feature objects at points that have been requested so far.
48 */
49var feature_list = [];
50
51/**
52 * Get a feature object at the given point, or creates one if it does not exist.
53 * @param {point} point The point to check
54 * @return {feature} The feature object at the point. Note that an empty name
55 *     indicates no feature
56 */
57function checkFeature(point) {
58  var feature;
59  // Check if there is already a feature object for the given point
60  for (var i = 0; i < feature_list.length; i++) {
61    feature = feature_list[i];
62    if (feature.location.latitude === point.latitude &&
63        feature.location.longitude === point.longitude) {
64      return feature;
65    }
66  }
67  var name = '';
68  feature = {
69    name: name,
70    location: point
71  };
72  return feature;
73}
74
75/**
76 * getFeature request handler. Gets a request with a point, and responds with a
77 * feature object indicating whether there is a feature at that point.
78 * @param {EventEmitter} call Call object for the handler to process
79 * @param {function(Error, feature)} callback Response callback
80 */
81function getFeature(call, callback) {
82  callback(null, checkFeature(call.request));
83}
84
85/**
86 * listFeatures request handler. Gets a request with two points, and responds
87 * with a stream of all features in the bounding box defined by those points.
88 * @param {Writable} call Writable stream for responses with an additional
89 *     request property for the request value.
90 */
91function listFeatures(call) {
92  var lo = call.request.lo;
93  var hi = call.request.hi;
94  var left = _.min([lo.longitude, hi.longitude]);
95  var right = _.max([lo.longitude, hi.longitude]);
96  var top = _.max([lo.latitude, hi.latitude]);
97  var bottom = _.min([lo.latitude, hi.latitude]);
98  // For each feature, check if it is in the given bounding box
99  _.each(feature_list, function(feature) {
100    if (feature.name === '') {
101      return;
102    }
103    if (feature.location.longitude >= left &&
104        feature.location.longitude <= right &&
105        feature.location.latitude >= bottom &&
106        feature.location.latitude <= top) {
107      call.write(feature);
108    }
109  });
110  call.end();
111}
112
113/**
114 * Calculate the distance between two points using the "haversine" formula.
115 * The formula is based on http://mathforum.org/library/drmath/view/51879.html.
116 * @param start The starting point
117 * @param end The end point
118 * @return The distance between the points in meters
119 */
120function getDistance(start, end) {
121  function toRadians(num) {
122    return num * Math.PI / 180;
123  }
124  var R = 6371000;  // earth radius in metres
125  var lat1 = toRadians(start.latitude / COORD_FACTOR);
126  var lat2 = toRadians(end.latitude / COORD_FACTOR);
127  var lon1 = toRadians(start.longitude / COORD_FACTOR);
128  var lon2 = toRadians(end.longitude / COORD_FACTOR);
129
130  var deltalat = lat2-lat1;
131  var deltalon = lon2-lon1;
132  var a = Math.sin(deltalat/2) * Math.sin(deltalat/2) +
133      Math.cos(lat1) * Math.cos(lat2) *
134      Math.sin(deltalon/2) * Math.sin(deltalon/2);
135  var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
136  return R * c;
137}
138
139/**
140 * recordRoute handler. Gets a stream of points, and responds with statistics
141 * about the "trip": number of points, number of known features visited, total
142 * distance traveled, and total time spent.
143 * @param {Readable} call The request point stream.
144 * @param {function(Error, routeSummary)} callback The callback to pass the
145 *     response to
146 */
147function recordRoute(call, callback) {
148  var point_count = 0;
149  var feature_count = 0;
150  var distance = 0;
151  var previous = null;
152  // Start a timer
153  var start_time = process.hrtime();
154  call.on('data', function(point) {
155    point_count += 1;
156    if (checkFeature(point).name !== '') {
157      feature_count += 1;
158    }
159    /* For each point after the first, add the incremental distance from the
160     * previous point to the total distance value */
161    if (previous != null) {
162      distance += getDistance(previous, point);
163    }
164    previous = point;
165  });
166  call.on('end', function() {
167    callback(null, {
168      point_count: point_count,
169      feature_count: feature_count,
170      // Cast the distance to an integer
171      distance: distance|0,
172      // End the timer
173      elapsed_time: process.hrtime(start_time)[0]
174    });
175  });
176}
177
178var route_notes = {};
179
180/**
181 * Turn the point into a dictionary key.
182 * @param {point} point The point to use
183 * @return {string} The key for an object
184 */
185function pointKey(point) {
186  return point.latitude + ' ' + point.longitude;
187}
188
189/**
190 * routeChat handler. Receives a stream of message/location pairs, and responds
191 * with a stream of all previous messages at each of those locations.
192 * @param {Duplex} call The stream for incoming and outgoing messages
193 */
194function routeChat(call) {
195  call.on('data', function(note) {
196    var key = pointKey(note.location);
197    /* For each note sent, respond with all previous notes that correspond to
198     * the same point */
199    if (route_notes.hasOwnProperty(key)) {
200      _.each(route_notes[key], function(note) {
201        call.write(note);
202      });
203    } else {
204      route_notes[key] = [];
205    }
206    // Then add the new note to the list
207    route_notes[key].push(JSON.parse(JSON.stringify(note)));
208  });
209  call.on('end', function() {
210    call.end();
211  });
212}
213
214/**
215 * Get a new server with the handler functions in this file bound to the methods
216 * it serves.
217 * @return {Server} The new server object
218 */
219function getServer() {
220  var server = new grpc.Server();
221  server.addProtoService(routeguide.RouteGuide.service, {
222    getFeature: getFeature,
223    listFeatures: listFeatures,
224    recordRoute: recordRoute,
225    routeChat: routeChat
226  });
227  return server;
228}
229
230if (require.main === module) {
231  // If this is run as a script, start a server on an unused port
232  var routeServer = getServer();
233  routeServer.bind('0.0.0.0:50051', grpc.ServerCredentials.createInsecure());
234  var argv = parseArgs(process.argv, {
235    string: 'db_path'
236  });
237  fs.readFile(path.resolve(argv.db_path), function(err, data) {
238    if (err) throw err;
239    feature_list = JSON.parse(data);
240    routeServer.start();
241  });
242}
243
244exports.getServer = getServer;
245