blob: eecac623544b064b8d67e252cadb2c8de47b943c [file] [log] [blame]
/*
*
* Copyright 2015 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
var messages = require('./route_guide_pb');
var services = require('./route_guide_grpc_pb');
var fs = require('fs');
var parseArgs = require('minimist');
var path = require('path');
var _ = require('lodash');
var grpc = require('grpc');
var COORD_FACTOR = 1e7;
/**
* For simplicity, a point is a record type that looks like
* {latitude: number, longitude: number}, and a feature is a record type that
* looks like {name: string, location: point}. feature objects with name===''
* are points with no feature.
*/
/**
* List of feature objects at points that have been requested so far.
*/
var feature_list = [];
/**
* Get a feature object at the given point, or creates one if it does not exist.
* @param {point} point The point to check
* @return {feature} The feature object at the point. Note that an empty name
* indicates no feature
*/
function checkFeature(point) {
var feature;
// Check if there is already a feature object for the given point
for (var i = 0; i < feature_list.length; i++) {
feature = feature_list[i];
if (feature.getLocation().getLatitude() === point.getLatitude() &&
feature.getLocation().getLongitude() === point.getLongitude()) {
return feature;
}
}
var name = '';
feature = new messages.Feature();
feature.setName(name);
feature.setLocation(point);
return feature;
}
/**
* getFeature request handler. Gets a request with a point, and responds with a
* feature object indicating whether there is a feature at that point.
* @param {EventEmitter} call Call object for the handler to process
* @param {function(Error, feature)} callback Response callback
*/
function getFeature(call, callback) {
callback(null, checkFeature(call.request));
}
/**
* listFeatures request handler. Gets a request with two points, and responds
* with a stream of all features in the bounding box defined by those points.
* @param {Writable} call Writable stream for responses with an additional
* request property for the request value.
*/
function listFeatures(call) {
var lo = call.request.getLo();
var hi = call.request.getHi();
var left = _.min([lo.getLongitude(), hi.getLongitude()]);
var right = _.max([lo.getLongitude(), hi.getLongitude()]);
var top = _.max([lo.getLatitude(), hi.getLatitude()]);
var bottom = _.min([lo.getLatitude(), hi.getLatitude()]);
// For each feature, check if it is in the given bounding box
_.each(feature_list, function(feature) {
if (feature.getName() === '') {
return;
}
if (feature.getLocation().getLongitude() >= left &&
feature.getLocation().getLongitude() <= right &&
feature.getLocation().getLatitude() >= bottom &&
feature.getLocation().getLatitude() <= top) {
call.write(feature);
}
});
call.end();
}
/**
* Calculate the distance between two points using the "haversine" formula.
* The formula is based on http://mathforum.org/library/drmath/view/51879.html.
* @param start The starting point
* @param end The end point
* @return The distance between the points in meters
*/
function getDistance(start, end) {
function toRadians(num) {
return num * Math.PI / 180;
}
var R = 6371000; // earth radius in metres
var lat1 = toRadians(start.getLatitude() / COORD_FACTOR);
var lat2 = toRadians(end.getLatitude() / COORD_FACTOR);
var lon1 = toRadians(start.getLongitude() / COORD_FACTOR);
var lon2 = toRadians(end.getLongitude() / COORD_FACTOR);
var deltalat = lat2-lat1;
var deltalon = lon2-lon1;
var a = Math.sin(deltalat/2) * Math.sin(deltalat/2) +
Math.cos(lat1) * Math.cos(lat2) *
Math.sin(deltalon/2) * Math.sin(deltalon/2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c;
}
/**
* recordRoute handler. Gets a stream of points, and responds with statistics
* about the "trip": number of points, number of known features visited, total
* distance traveled, and total time spent.
* @param {Readable} call The request point stream.
* @param {function(Error, routeSummary)} callback The callback to pass the
* response to
*/
function recordRoute(call, callback) {
var point_count = 0;
var feature_count = 0;
var distance = 0;
var previous = null;
// Start a timer
var start_time = process.hrtime();
call.on('data', function(point) {
point_count += 1;
if (checkFeature(point).name !== '') {
feature_count += 1;
}
/* For each point after the first, add the incremental distance from the
* previous point to the total distance value */
if (previous != null) {
distance += getDistance(previous, point);
}
previous = point;
});
call.on('end', function() {
var summary = new messages.RouteSummary();
summary.setPointCount(point_count);
summary.setFeatureCount(feature_count);
// Cast the distance to an integer
summary.setDistance(distance|0);
// End the timer
summary.setElapsedTime(process.hrtime(start_time)[0]);
callback(null, summary);
});
}
var route_notes = {};
/**
* Turn the point into a dictionary key.
* @param {point} point The point to use
* @return {string} The key for an object
*/
function pointKey(point) {
return point.getLatitude() + ' ' + point.getLongitude();
}
/**
* routeChat handler. Receives a stream of message/location pairs, and responds
* with a stream of all previous messages at each of those locations.
* @param {Duplex} call The stream for incoming and outgoing messages
*/
function routeChat(call) {
call.on('data', function(note) {
var key = pointKey(note.getLocation());
/* For each note sent, respond with all previous notes that correspond to
* the same point */
if (route_notes.hasOwnProperty(key)) {
_.each(route_notes[key], function(note) {
call.write(note);
});
} else {
route_notes[key] = [];
}
// Then add the new note to the list
route_notes[key].push(note);
});
call.on('end', function() {
call.end();
});
}
/**
* Get a new server with the handler functions in this file bound to the methods
* it serves.
* @return {Server} The new server object
*/
function getServer() {
var server = new grpc.Server();
server.addService(services.RouteGuideService, {
getFeature: getFeature,
listFeatures: listFeatures,
recordRoute: recordRoute,
routeChat: routeChat
});
return server;
}
if (require.main === module) {
// If this is run as a script, start a server on an unused port
var routeServer = getServer();
routeServer.bind('0.0.0.0:50051', grpc.ServerCredentials.createInsecure());
var argv = parseArgs(process.argv, {
string: 'db_path'
});
fs.readFile(path.resolve(argv.db_path), function(err, data) {
if (err) throw err;
// Transform the loaded features to Feature objects
feature_list = _.map(JSON.parse(data), function(value) {
var feature = new messages.Feature();
feature.setName(value.name);
var location = new messages.Point();
location.setLatitude(value.location.latitude);
location.setLongitude(value.location.longitude);
feature.setLocation(location);
return feature;
});
routeServer.start();
});
}
exports.getServer = getServer;