Skip to content

Commit

Permalink
feat: add nearbyGPSCoordinate function to Location module (#945)
Browse files Browse the repository at this point in the history
* implemented feature nearbyGPSCoordinate()

* added tests

* fixed msvc build
  • Loading branch information
filif123 authored Oct 4, 2024
1 parent 9ccf41f commit 37ee841
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 0 deletions.
23 changes: 23 additions & 0 deletions include/faker-cxx/location.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <string>
#include <string_view>
#include <limits>

#include "faker-cxx/export.h"
#include "faker-cxx/types/locale.h"
Expand Down Expand Up @@ -150,6 +151,28 @@ FAKER_CXX_EXPORT std::string latitude(Precision precision = Precision::FourDp);
*/
FAKER_CXX_EXPORT std::string longitude(Precision precision = Precision::FourDp);

/**
* @brief Generates a random GPS coordinate within the specified radius from the given coordinate.
*
* @param precision The number of decimal points of precision for the latitude and longitude. Defaults to `Precision::FourDp`.
* @param origin The origin GPS coordinate. Defaults to a random GPS coordinate.
* @param radius The radius in kilometers or miles. Defaults to 10.
* @param isMetric The unit of radius. Defaults to false which means miles.
*
* @returns GPS coordinate within the specified radius from the given coordinate.
*
* @code
* faker::location::nearbyGPSCoordinate() // "48.8566", "2.3522"
* faker::location::nearbyGPSCoordinate(Precision::FourDp, {33, -170}) // "33.0165", "-170.0636"
* faker::location::nearbyGPSCoordinate(Precision::FourDp, {33, -170}, 1000, true) // "37.9163", "-179.2408"
* @endcode
*/
FAKER_CXX_EXPORT std::tuple<std::string, std::string> nearbyGPSCoordinate(
Precision precision = Precision::FourDp,
const std::tuple<double, double>& origin = { std::numeric_limits<double>::max(), std::numeric_limits<double>::max() },
double radius = 10,
bool isMetric = false);

/**
* @brief Generates a random direction from cardinal and ordinal directions.
*
Expand Down
46 changes: 46 additions & 0 deletions src/modules/location.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
#include "faker-cxx/types/precision.h"
#include "location_data.h"

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

namespace faker::location
{
namespace
Expand Down Expand Up @@ -213,4 +217,46 @@ std::string_view timeZone()
return helper::randomElement(timeZones);
}

std::tuple<std::string, std::string> nearbyGPSCoordinate(
Precision precision,
const std::tuple<double, double>& origin,
const double radius,
const bool isMetric)
{
// If origin is not provided, generate a random GPS coordinate.
if (std::get<0>(origin) == std::numeric_limits<double>::max() &&
std::get<1>(origin) == std::numeric_limits<double>::max())
{
return { latitude(precision), longitude(precision) };
}

const auto angleRadians = number::decimal<double>(2 * M_PI);

const auto radiusMetric = isMetric ? radius : radius * 1.60934;
const auto distanceInKm = number::decimal<double>(radiusMetric);

constexpr auto kmPerDegree = 40000 / 360; //The distance in km per degree for earth.
const auto distanceInDegree = distanceInKm / kmPerDegree;

auto coordinateLatitude = std::get<0>(origin) + distanceInDegree * std::sin(angleRadians);
auto coordinateLongitude = std::get<1>(origin) + distanceInDegree * std::cos(angleRadians);

// Box the latitude [-90, 90]
coordinateLatitude = std::fmod(coordinateLatitude, 180.0);
if (coordinateLatitude < -90.0 || coordinateLatitude > 90.0)
{
coordinateLatitude = std::copysign(180.0, coordinateLatitude) - coordinateLatitude;
coordinateLongitude += 180.0;
}

// Box the longitude [-180, 180]
coordinateLongitude = std::fmod(std::fmod(coordinateLongitude, 360.0) + 540.0, 360.0) - 180.0;

return
{
common::precisionFormat(precision, coordinateLatitude),
common::precisionFormat(precision, coordinateLongitude)
};
}

}
96 changes: 96 additions & 0 deletions tests/modules/location_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <string>
#include <string_view>
#include <vector>
#include <cmath>

#include "gtest/gtest.h"

Expand All @@ -12,6 +13,10 @@
#include "person_data.h"
#include "string_data.h"

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

using namespace ::testing;
using namespace faker;
using namespace faker::location;
Expand Down Expand Up @@ -122,6 +127,24 @@ class LocationTest : public TestWithParam<Locale>
return std::ranges::any_of(string::numericCharacters,
[character](char numericCharacter) { return numericCharacter == character; });
}

static constexpr double EARTH_RADIUS_KM = 6371.0;

static double haversine(double lat1, double lon1, double lat2, double lon2) {
auto toRadians = [](double degree) -> double {
return degree * M_PI / 180.0;
};

const double dLat = toRadians(lat2 - lat1);
const double dLon = toRadians(lon2 - lon1);

lat1 = toRadians(lat1);
lat2 = toRadians(lat2);

const double a = std::pow(std::sin(dLat / 2), 2) +
std::pow(std::sin(dLon / 2), 2) * std::cos(lat1) * std::cos(lat2);
return 2 * EARTH_RADIUS_KM * std::atan2(std::sqrt(a), std::sqrt(1 - a));
}
};

TEST_P(LocationTest, shouldGenerateState)
Expand Down Expand Up @@ -507,6 +530,79 @@ TEST_F(LocationTest, shouldGenerateLongitudeWithSpecifiedPrecision)
ASSERT_LE(longitudeAsFloat, 180);
}

TEST_F(LocationTest, shouldGenerateNearbyGPSCoordinateWithoutOrigin)
{
const auto generatedNearbyGPSCoordinate = nearbyGPSCoordinate();

auto offset = std::get<0>(generatedNearbyGPSCoordinate).size();
const auto latitudeAsFloat = std::stof(std::get<0>(generatedNearbyGPSCoordinate), &offset);

offset = std::get<1>(generatedNearbyGPSCoordinate).size();
const auto longitudeAsFloat = std::stof(std::get<1>(generatedNearbyGPSCoordinate), &offset);

const auto generatedLatitudeParts = common::split(std::get<0>(generatedNearbyGPSCoordinate), ".");
const auto generatedLongitudeParts = common::split(std::get<1>(generatedNearbyGPSCoordinate), ".");

ASSERT_EQ(generatedLatitudeParts.size(), 2);
ASSERT_EQ(generatedLatitudeParts[1].size(), 4);
ASSERT_GE(latitudeAsFloat, -90);
ASSERT_LE(latitudeAsFloat, 90);

ASSERT_EQ(generatedLongitudeParts.size(), 2);
ASSERT_EQ(generatedLongitudeParts[1].size(), 4);
ASSERT_GE(longitudeAsFloat, -180);
ASSERT_LE(longitudeAsFloat, 180);
}

TEST_F(LocationTest, shouldGenerateNearbyGPSCoordinateWithOriginInKilometers)
{
constexpr std::tuple origin{0, 0};
const auto generatedNearbyGPSCoordinate = nearbyGPSCoordinate(Precision::ThreeDp, origin, 10, true);

auto offset = std::get<0>(generatedNearbyGPSCoordinate).size();
const auto latitudeAsFloat = std::stof(std::get<0>(generatedNearbyGPSCoordinate), &offset);

offset = std::get<1>(generatedNearbyGPSCoordinate).size();
const auto longitudeAsFloat = std::stof(std::get<1>(generatedNearbyGPSCoordinate), &offset);

const auto generatedLatitudeParts = common::split(std::get<0>(generatedNearbyGPSCoordinate), ".");
const auto generatedLongitudeParts = common::split(std::get<1>(generatedNearbyGPSCoordinate), ".");

ASSERT_EQ(generatedLatitudeParts.size(), 2);
ASSERT_EQ(generatedLatitudeParts[1].size(), 3);
ASSERT_EQ(generatedLongitudeParts.size(), 2);
ASSERT_EQ(generatedLongitudeParts[1].size(), 3);

const auto distance = haversine(std::get<0>(origin), std::get<1>(origin), latitudeAsFloat, longitudeAsFloat);

ASSERT_LE(distance, 10.0);
}

TEST_F(LocationTest, shouldGenerateNearbyGPSCoordinateWithOriginInMiles)
{
constexpr std::tuple origin{0, 0};
const auto generatedNearbyGPSCoordinate = nearbyGPSCoordinate(Precision::ThreeDp, origin, 10, false);

auto offset = std::get<0>(generatedNearbyGPSCoordinate).size();
const auto latitudeAsFloat = std::stof(std::get<0>(generatedNearbyGPSCoordinate), &offset);

offset = std::get<1>(generatedNearbyGPSCoordinate).size();
const auto longitudeAsFloat = std::stof(std::get<1>(generatedNearbyGPSCoordinate), &offset);

const auto generatedLatitudeParts = common::split(std::get<0>(generatedNearbyGPSCoordinate), ".");
const auto generatedLongitudeParts = common::split(std::get<1>(generatedNearbyGPSCoordinate), ".");

ASSERT_EQ(generatedLatitudeParts.size(), 2);
ASSERT_EQ(generatedLatitudeParts[1].size(), 3);
ASSERT_EQ(generatedLongitudeParts.size(), 2);
ASSERT_EQ(generatedLongitudeParts[1].size(), 3);

const auto distanceKm = haversine(std::get<0>(origin), std::get<1>(origin), latitudeAsFloat, longitudeAsFloat);
const auto distanceMiles = distanceKm * 0.621371;

ASSERT_LE(distanceMiles, 10.0);
}

TEST_F(LocationTest, shouldGenerateDirection)
{
const auto generatedDirection = direction();
Expand Down

0 comments on commit 37ee841

Please sign in to comment.