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 messages = require('./route_guide_pb'); 20var services = require('./route_guide_grpc_pb'); 21 22var fs = require('fs'); 23var parseArgs = require('minimist'); 24var path = require('path'); 25var _ = require('lodash'); 26var grpc = require('grpc'); 27 28var COORD_FACTOR = 1e7; 29 30/** 31 * For simplicity, a point is a record type that looks like 32 * {latitude: number, longitude: number}, and a feature is a record type that 33 * looks like {name: string, location: point}. feature objects with name==='' 34 * are points with no feature. 35 */ 36 37/** 38 * List of feature objects at points that have been requested so far. 39 */ 40var feature_list = []; 41 42/** 43 * Get a feature object at the given point, or creates one if it does not exist. 44 * @param {point} point The point to check 45 * @return {feature} The feature object at the point. Note that an empty name 46 * indicates no feature 47 */ 48function checkFeature(point) { 49 var feature; 50 // Check if there is already a feature object for the given point 51 for (var i = 0; i < feature_list.length; i++) { 52 feature = feature_list[i]; 53 if (feature.getLocation().getLatitude() === point.getLatitude() && 54 feature.getLocation().getLongitude() === point.getLongitude()) { 55 return feature; 56 } 57 } 58 var name = ''; 59 feature = new messages.Feature(); 60 feature.setName(name); 61 feature.setLocation(point); 62 return feature; 63} 64 65/** 66 * getFeature request handler. Gets a request with a point, and responds with a 67 * feature object indicating whether there is a feature at that point. 68 * @param {EventEmitter} call Call object for the handler to process 69 * @param {function(Error, feature)} callback Response callback 70 */ 71function getFeature(call, callback) { 72 callback(null, checkFeature(call.request)); 73} 74 75/** 76 * listFeatures request handler. Gets a request with two points, and responds 77 * with a stream of all features in the bounding box defined by those points. 78 * @param {Writable} call Writable stream for responses with an additional 79 * request property for the request value. 80 */ 81function listFeatures(call) { 82 var lo = call.request.getLo(); 83 var hi = call.request.getHi(); 84 var left = _.min([lo.getLongitude(), hi.getLongitude()]); 85 var right = _.max([lo.getLongitude(), hi.getLongitude()]); 86 var top = _.max([lo.getLatitude(), hi.getLatitude()]); 87 var bottom = _.min([lo.getLatitude(), hi.getLatitude()]); 88 // For each feature, check if it is in the given bounding box 89 _.each(feature_list, function(feature) { 90 if (feature.getName() === '') { 91 return; 92 } 93 if (feature.getLocation().getLongitude() >= left && 94 feature.getLocation().getLongitude() <= right && 95 feature.getLocation().getLatitude() >= bottom && 96 feature.getLocation().getLatitude() <= top) { 97 call.write(feature); 98 } 99 }); 100 call.end(); 101} 102 103/** 104 * Calculate the distance between two points using the "haversine" formula. 105 * The formula is based on http://mathforum.org/library/drmath/view/51879.html. 106 * @param start The starting point 107 * @param end The end point 108 * @return The distance between the points in meters 109 */ 110function getDistance(start, end) { 111 function toRadians(num) { 112 return num * Math.PI / 180; 113 } 114 var R = 6371000; // earth radius in metres 115 var lat1 = toRadians(start.getLatitude() / COORD_FACTOR); 116 var lat2 = toRadians(end.getLatitude() / COORD_FACTOR); 117 var lon1 = toRadians(start.getLongitude() / COORD_FACTOR); 118 var lon2 = toRadians(end.getLongitude() / COORD_FACTOR); 119 120 var deltalat = lat2-lat1; 121 var deltalon = lon2-lon1; 122 var a = Math.sin(deltalat/2) * Math.sin(deltalat/2) + 123 Math.cos(lat1) * Math.cos(lat2) * 124 Math.sin(deltalon/2) * Math.sin(deltalon/2); 125 var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); 126 return R * c; 127} 128 129/** 130 * recordRoute handler. Gets a stream of points, and responds with statistics 131 * about the "trip": number of points, number of known features visited, total 132 * distance traveled, and total time spent. 133 * @param {Readable} call The request point stream. 134 * @param {function(Error, routeSummary)} callback The callback to pass the 135 * response to 136 */ 137function recordRoute(call, callback) { 138 var point_count = 0; 139 var feature_count = 0; 140 var distance = 0; 141 var previous = null; 142 // Start a timer 143 var start_time = process.hrtime(); 144 call.on('data', function(point) { 145 point_count += 1; 146 if (checkFeature(point).name !== '') { 147 feature_count += 1; 148 } 149 /* For each point after the first, add the incremental distance from the 150 * previous point to the total distance value */ 151 if (previous != null) { 152 distance += getDistance(previous, point); 153 } 154 previous = point; 155 }); 156 call.on('end', function() { 157 var summary = new messages.RouteSummary(); 158 summary.setPointCount(point_count); 159 summary.setFeatureCount(feature_count); 160 // Cast the distance to an integer 161 summary.setDistance(distance|0); 162 // End the timer 163 summary.setElapsedTime(process.hrtime(start_time)[0]); 164 callback(null, summary); 165 }); 166} 167 168var route_notes = {}; 169 170/** 171 * Turn the point into a dictionary key. 172 * @param {point} point The point to use 173 * @return {string} The key for an object 174 */ 175function pointKey(point) { 176 return point.getLatitude() + ' ' + point.getLongitude(); 177} 178 179/** 180 * routeChat handler. Receives a stream of message/location pairs, and responds 181 * with a stream of all previous messages at each of those locations. 182 * @param {Duplex} call The stream for incoming and outgoing messages 183 */ 184function routeChat(call) { 185 call.on('data', function(note) { 186 var key = pointKey(note.getLocation()); 187 /* For each note sent, respond with all previous notes that correspond to 188 * the same point */ 189 if (route_notes.hasOwnProperty(key)) { 190 _.each(route_notes[key], function(note) { 191 call.write(note); 192 }); 193 } else { 194 route_notes[key] = []; 195 } 196 // Then add the new note to the list 197 route_notes[key].push(note); 198 }); 199 call.on('end', function() { 200 call.end(); 201 }); 202} 203 204/** 205 * Get a new server with the handler functions in this file bound to the methods 206 * it serves. 207 * @return {Server} The new server object 208 */ 209function getServer() { 210 var server = new grpc.Server(); 211 server.addService(services.RouteGuideService, { 212 getFeature: getFeature, 213 listFeatures: listFeatures, 214 recordRoute: recordRoute, 215 routeChat: routeChat 216 }); 217 return server; 218} 219 220if (require.main === module) { 221 // If this is run as a script, start a server on an unused port 222 var routeServer = getServer(); 223 routeServer.bind('0.0.0.0:50051', grpc.ServerCredentials.createInsecure()); 224 var argv = parseArgs(process.argv, { 225 string: 'db_path' 226 }); 227 fs.readFile(path.resolve(argv.db_path), function(err, data) { 228 if (err) throw err; 229 // Transform the loaded features to Feature objects 230 feature_list = _.map(JSON.parse(data), function(value) { 231 var feature = new messages.Feature(); 232 feature.setName(value.name); 233 var location = new messages.Point(); 234 location.setLatitude(value.location.latitude); 235 location.setLongitude(value.location.longitude); 236 feature.setLocation(location); 237 return feature; 238 }); 239 routeServer.start(); 240 }); 241} 242 243exports.getServer = getServer; 244