As a client user, I want to frequently request a list of all other users (along with their geographic coordinates) whose current position lies within a rectangular map area so that I can represent their positions on a map.
The GeoServer provides support for client applications seeking to track the geographic location of large numbers of users (don’t worry, it’s not for sale to your government, and I’ll divulge your location only with your permission). Clients will typically be map-centric phone applications. We’ll concentrate only on building parts of the server, so use your imagination as to what the client might look like.
A client registers with the server to begin tracking the location of its user. The client transmits location updates to the server from time to time.
Here’s the current code for the GeoServer (including the header file for the Location class):
c9/1/GeoServerTest.cpp | |
| #include "CppUTest/TestHarness.h" |
| #include "CppUTestExtensions.h" |
| #include "GeoServer.h" |
| |
| using namespace std; |
| |
| TEST_GROUP(AGeoServer) { |
| GeoServer server; |
| |
| const string aUser{"auser"}; |
| const double LocationTolerance{0.005}; |
| }; |
| TEST(AGeoServer, TracksAUser) { |
| server.track(aUser); |
| |
| CHECK_TRUE(server.isTracking(aUser)); |
| } |
| |
| TEST(AGeoServer, IsNotTrackingAUserNotTracked) { |
| CHECK_FALSE(server.isTracking(aUser)); |
| } |
| |
| TEST(AGeoServer, TracksMultipleUsers) { |
| server.track(aUser); |
| server.track("anotheruser"); |
| |
| CHECK_FALSE(server.isTracking("thirduser")); |
| CHECK_TRUE(server.isTracking(aUser)); |
| CHECK_TRUE(server.isTracking("anotheruser")); |
| } |
| |
| TEST(AGeoServer, IsTrackingAnswersFalseWhenUserNoLongerTracked) { |
| server.track(aUser); |
| server.stopTracking(aUser); |
| |
| CHECK_FALSE(server.isTracking(aUser)); |
| } |
| |
| TEST(AGeoServer, UpdatesLocationOfUser) { |
| server.track(aUser); |
| server.updateLocation(aUser, Location{38, -104}); |
| |
| auto location = server.locationOf(aUser); |
| DOUBLES_EQUAL(38, location.latitude(), LocationTolerance); |
| DOUBLES_EQUAL(-104, location.longitude(), LocationTolerance); |
| } |
| |
| TEST(AGeoServer, AnswersUnknownLocationForUserNotTracked) { |
| CHECK_TRUE(server.locationOf("anAbUser").isUnknown()); |
| } |
| |
| TEST(AGeoServer, AnswersUnknownLocationForTrackedUserWithNoLocationUpdate) { |
| server.track(aUser); |
| CHECK_TRUE(server.locationOf(aUser).isUnknown()); |
| } |
| |
| TEST(AGeoServer, AnswersUnknownLocationForUserNoLongerTracked) { |
| server.track(aUser); |
| server.updateLocation(aUser, Location(40, 100)); |
| server.stopTracking(aUser); |
| CHECK_TRUE(server.locationOf(aUser).isUnknown()); |
| } |
c9/1/GeoServer.h | |
| #ifndef GeoServer_h |
| #define GeoServer_h |
| |
| #include <string> |
| #include <unordered_map> |
| |
| #include "Location.h" |
| |
| class GeoServer { |
| public: |
| void track(const std::string& user); |
| void stopTracking(const std::string& user); |
| void updateLocation(const std::string& user, const Location& location); |
| |
| bool isTracking(const std::string& user) const; |
| Location locationOf(const std::string& user) const; |
| |
| private: |
| std::unordered_map<std::string, Location> locations_; |
| |
| std::unordered_map<std::string, Location>::const_iterator |
| find(const std::string& user) const; |
| }; |
| |
| #endif |
c9/1/GeoServer.cpp | |
| #include "GeoServer.h" |
| #include "Location.h" |
| using namespace std; |
| void GeoServer::track(const string& user) { |
| locations_[user] = Location(); |
| } |
| |
| void GeoServer::stopTracking(const string& user) { |
| locations_.erase(user); |
| } |
| |
| bool GeoServer::isTracking(const string& user) const { |
| return find(user) != locations_.end(); |
| } |
| |
| void GeoServer::updateLocation(const string& user, const Location& location) { |
| locations_[user] = location; |
| } |
| Location GeoServer::locationOf(const string& user) const { |
| if (!isTracking(user)) return Location{}; // TODO performance cost? |
| return find(user)->second; |
| } |
| |
| std::unordered_map<std::string, Location>::const_iterator |
| GeoServer::find(const std::string& user) const { |
| return locations_.find(user); |
| } |
c9/1/Location.h | |
| #ifndef Location_h |
| #define Location_h |
| |
| #include <limits> |
| #include <cmath> |
| #include <ostream> |
| |
| const double Pi{ 4.0 * atan(1.0) }; |
| const double ToRadiansConversionFactor{ Pi / 180 }; |
| const double RadiusOfEarthInMeters{ 6372000 }; |
| const double MetersPerDegreeAtEquator{ 111111 }; |
| |
| const double North{ 0 }; |
| const double West{ 90 }; |
| const double South{ 180 }; |
| const double East{ 270 }; |
| const double CloseMeters{ 3 }; |
| |
| class Location { |
| public: |
| Location(); |
| Location(double latitude, double longitude); |
| |
| inline double toRadians(double degrees) const { |
| return degrees * ToRadiansConversionFactor; |
| } |
| |
| inline double toCoordinate(double radians) const { |
| return radians * (180 / Pi); |
| } |
| |
| inline double latitudeAsRadians() const { |
| return toRadians(latitude_); |
| } |
| |
| inline double longitudeAsRadians() const { |
| return toRadians(longitude_); |
| } |
| |
| double latitude() const; |
| double longitude() const; |
| |
| bool operator==(const Location& that); |
| bool operator!=(const Location& that); |
| |
| Location go(double meters, double bearing) const; |
| double distanceInMeters(const Location& there) const; |
| bool isUnknown() const; |
| bool isVeryCloseTo(const Location& there) const; |
| |
| private: |
| double latitude_; |
| double longitude_; |
| |
| double haversineDistance(Location there) const; |
| }; |
| |
| std::ostream& operator<<(std::ostream& output, const Location& location); |
| |
| #endif |
You can refer to the source distribution for other source files not listed here. Hmmm...a comment in GeoServer.cpp! Someone is worried about the potential performance cost of looking up into the locations_ map twice. We’ll worry about that later (see TDD and Performance).
With the simple stuff out of the way, let’s build something meaty into the GeoServer.
As a client user, I want to frequently request a list of all other users (along with their geographic coordinates) whose current position lies within a rectangular map area so that I can represent their positions on a map.
We build an implementation into GeoServer.
c9/2/GeoServerTest.cpp | |
| TEST_GROUP(AGeoServer_UsersInBox) { |
| GeoServer server; |
| |
| const double TenMeters { 10 }; |
| const double Width { 2000 + TenMeters }; |
| const double Height { 4000 + TenMeters}; |
| const string aUser { "auser" }; |
| const string bUser { "buser" }; |
| const string cUser { "cuser" }; |
| |
| Location aUserLocation { 38, -103 }; |
| |
| void setup() override { |
| server.track(aUser); |
| server.track(bUser); |
| server.track(cUser); |
| server.updateLocation(aUser, aUserLocation); |
| } |
| vector<string> UserNames(const vector<User>& users) { |
| return Collect<User,string>(users, [](User each) { return each.name(); }); |
| } |
| }; |
| |
| TEST(AGeoServer_UsersInBox, AnswersUsersInSpecifiedRange) { |
| server.updateLocation( |
| bUser, Location{aUserLocation.go(Width / 2 - TenMeters, East)}); |
| |
| auto users = server.usersInBox(aUser, Width, Height); |
| |
| CHECK_EQUAL(vector<string> { bUser }, UserNames(users)); |
| } |
| |
| TEST(AGeoServer_UsersInBox, AnswersOnlyUsersWithinSpecifiedRange) { |
| server.updateLocation( |
| bUser, Location{aUserLocation.go(Width / 2 + TenMeters, East)}); |
| |
| server.updateLocation( |
| cUser, Location{aUserLocation.go(Width / 2 - TenMeters, East)}); |
| |
| auto users = server.usersInBox(aUser, Width, Height); |
| |
| CHECK_EQUAL(vector<string> { cUser }, UserNames(users)); |
| } |
c9/2/GeoServer.cpp | |
| bool GeoServer::isDifferentUserInBounds( |
| const pair<string, Location>& each, |
| const string& user, |
| const Area& box) const { |
| if (each.first == user) return false; |
| return box.inBounds(each.second); |
| } |
| |
| vector<User> GeoServer::usersInBox( |
| const string& user, double widthInMeters, double heightInMeters) const { |
| auto location = locations_.find(user)->second; |
| Area box { location, widthInMeters, heightInMeters }; |
| |
| vector<User> users; |
| for (auto& each: locations_) |
| if (isDifferentUserInBounds(each, user, box)) |
| users.push_back(User{each.first, each.second}); |
| return users; |
| } |
An Area is a rectangle centered around a Location. It can answer whether it contains a(nother) location. Here’s the header file for Area:
c9/2/Area.h | |
| #ifndef Area_h |
| #define Area_h |
| |
| #include "Location.h" |
| |
| class Area { |
| public: |
| Area(const Location& location, double width, double height); |
| Location upperLeft() const; |
| Location upperRight() const; |
| Location lowerRight() const; |
| Location lowerLeft() const; |
| bool inBounds(const Location&) const; |
| |
| private: |
| double left_; |
| double right_; |
| double top_; |
| double bottom_; |
| }; |
| |
| #endif |
A User is a container holding the user’s name and a location object.
c9/2/User.h | |
| #ifndef User_h |
| #define User_h |
| #include "Location.h" |
| |
| class User { |
| public: |
| User(const std::string& name, Location location) |
| : name_(name), location_(location) {} |
| std::string name() { return name_; } |
| Location location() { return location_; } |
| |
| private: |
| std::string name_; |
| Location location_; |
| }; |
| #endif |
3.136.17.12