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