Skip to content

Commit

Permalink
chore: simplify and fix impl
Browse files Browse the repository at this point in the history
  • Loading branch information
ST-DDT committed Apr 26, 2022
1 parent ab88a3f commit f380b82
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 119 deletions.
108 changes: 15 additions & 93 deletions src/address.ts
Original file line number Diff line number Diff line change
@@ -1,78 +1,9 @@
import type { Faker } from '.';

/**
* Converts degrees to radians.
*
* @param degrees Degrees.
* The distance in km per degree.
*/
function degreesToRadians(degrees: number): number {
return degrees * (Math.PI / 180.0);
}

/**
* Converts radians to degrees.
*
* @param radians Radians.
*/
function radiansToDegrees(radians: number): number {
return radians * (180.0 / Math.PI);
}

/**
* Converts kilometers to miles.
*
* @param miles Miles.
*/
function kilometersToMiles(miles: number): number {
return miles * 0.621371;
}

/**
* Calculates coordinates with offset.
*
* @param coordinate Coordinate.
* @param bearing Bearing.
* @param distance Distance.
* @param isMetric Metric: true, Miles: false.
*/
function coordinateWithOffset(
coordinate: [latitude: number, longitude: number],
bearing: number,
distance: number,
isMetric: boolean
): [latitude: number, longitude: number] {
const EQUATORIAL_EARTH_RADIUS = 6378.137; // http://nssdc.gsfc.nasa.gov/planetary/factsheet/earthfact.html
const d = isMetric ? distance : kilometersToMiles(distance); // Distance in km

const latitudeCenter = degreesToRadians(coordinate[0]); // Current latitude point converted to radians
const longitudeCenter = degreesToRadians(coordinate[1]); // Current longitude point converted to radians

const latitudeOffset = Math.asin(
Math.sin(latitudeCenter) * Math.cos(d / EQUATORIAL_EARTH_RADIUS) +
Math.cos(latitudeCenter) *
Math.sin(d / EQUATORIAL_EARTH_RADIUS) *
Math.cos(bearing)
);

let longitudeOffset =
longitudeCenter +
Math.atan2(
Math.sin(bearing) *
Math.sin(d / EQUATORIAL_EARTH_RADIUS) *
Math.cos(latitudeCenter),
Math.cos(d / EQUATORIAL_EARTH_RADIUS) -
Math.sin(latitudeCenter) * Math.sin(latitudeOffset)
);

// Keep longitude in range [-180, 180]
if (longitudeOffset > degreesToRadians(180)) {
longitudeOffset = longitudeOffset - degreesToRadians(360);
} else if (longitudeOffset < degreesToRadians(-180)) {
longitudeOffset = longitudeOffset + degreesToRadians(360);
}

return [radiansToDegrees(latitudeOffset), radiansToDegrees(longitudeOffset)];
}
const kmPerDegree = 40_000 / 360; // in km/°

/**
* Module to generate addresses and locations.
Expand Down Expand Up @@ -506,36 +437,27 @@ export class Address {
return [this.latitude(), this.longitude()];
}

const twoPi = 2 * Math.PI;
const angleRadians = Math.PI / 4; /* this.faker.datatype.float({
const angleRadians = this.faker.datatype.float({
min: 0,
max: twoPi,
max: 2 * Math.PI,
precision: 0.00001,
}); // in ° radians*/
}); // in ° radians

const radiusMetric = isMetric ? radius : radius * 1.60934; // in km
const totalDistance = 100 * 0.99; /*this.faker.datatype.float({
min: 0,
max: radiusMetric,
precision: 0.001,
}); // in km*/
const errorCorrection = 0.995; // avoid float issues
const distanceInKm =
this.faker.datatype.float({
min: 0,
max: radiusMetric,
precision: 0.001,
}) * errorCorrection; // in km

const kmPerDegreeLatitude = 111; // in km/°
const distanceLatitude = totalDistance / kmPerDegreeLatitude; // in °
const distanceInDegree = distanceInKm / kmPerDegree; // in °

const offsetLatitude = Math.sin(angleRadians) * distanceLatitude;
const offsetLatitude = Math.sin(angleRadians) * distanceInDegree;
const newLatitude = coordinate[0] + offsetLatitude;

const remainingDistance = Math.sqrt(
Math.pow(totalDistance, 2) -
Math.pow(offsetLatitude * kmPerDegreeLatitude, 2)
);

const kmPerDegreeLongitude =
Math.abs(Math.cos(degreesToRadians(newLatitude))) * 111; // in km/°
const distanceLongitude = remainingDistance / kmPerDegreeLongitude; // in °

const offsetLongitude = Math.cos(angleRadians) * distanceLongitude;
const offsetLongitude = Math.cos(angleRadians) * distanceInDegree;
const newLongitude = coordinate[1] + offsetLongitude;

const newCoordinate: [latitude: number, longitude: number] = [
Expand Down
47 changes: 21 additions & 26 deletions test/address.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const seededRuns = [
cardinalDirection: 'East',
cardinalDirectionAbbr: 'E',
timeZone: 'Europe/Amsterdam',
nearbyGpsCoordinates: ['-0.0811', '0.0816'],
nearbyGpsCoordinates: ['0.0814', '-0.0809'],
},
},
{
Expand Down Expand Up @@ -89,7 +89,7 @@ const seededRuns = [
cardinalDirection: 'East',
cardinalDirectionAbbr: 'E',
timeZone: 'Africa/Casablanca',
nearbyGpsCoordinates: ['-0.0061', '0.0808'],
nearbyGpsCoordinates: ['0.0806', '-0.0061'],
},
},
{
Expand Down Expand Up @@ -119,7 +119,7 @@ const seededRuns = [
cardinalDirection: 'West',
cardinalDirectionAbbr: 'W',
timeZone: 'Asia/Magadan',
nearbyGpsCoordinates: ['0.0597', '-0.0288'],
nearbyGpsCoordinates: ['-0.0287', '0.0596'],
},
},
];
Expand Down Expand Up @@ -584,17 +584,16 @@ describe('address', () => {
});
});

describe.only('nearbyGPSCoordinate()', () => {
describe('nearbyGPSCoordinate()', () => {
it('should return random gps coordinate within a distance of another one', () => {
for (let i = 0; i <= 10000; i++) {
faker.seed(i);
const latitudeFloat1 = +faker.address.latitude();
const longitudeFloat1 = +faker.address.longitude();
const radius = 100; // range of [1, 100)
const isMetric = true; // faker.datatype.boolean();
for (let i = 0; i < 100; i++) {
const latitude1 = +faker.address.latitude();
const longitude1 = +faker.address.longitude();
const radius = faker.datatype.float({ min: 1, max: 100 });
const isMetric = faker.datatype.boolean();

const coordinate = faker.address.nearbyGPSCoordinate(
[latitudeFloat1, longitudeFloat1],
[latitude1, longitude1],
radius,
isMetric
);
Expand All @@ -603,26 +602,22 @@ describe('address', () => {
expect(coordinate[0]).toBeTypeOf('string');
expect(coordinate[1]).toBeTypeOf('string');

const latitudeFloat2 = parseFloat(coordinate[0]);
expect(latitudeFloat2).toBeGreaterThanOrEqual(-90.0);
expect(latitudeFloat2).toBeLessThanOrEqual(90.0);
const latitude2 = +coordinate[0];
expect(latitude2).toBeGreaterThanOrEqual(-90.0);
expect(latitude2).toBeLessThanOrEqual(90.0);

const longitudeFloat2 = parseFloat(coordinate[1]);
expect(longitudeFloat2).toBeGreaterThanOrEqual(-180.0);
expect(longitudeFloat2).toBeLessThanOrEqual(180.0);
const longitude2 = +coordinate[1];
expect(longitude2).toBeGreaterThanOrEqual(-180.0);
expect(longitude2).toBeLessThanOrEqual(180.0);

// Due to floating point math, and constants that are not extremely precise,
// returned points will not be strictly within the given radius of the input
// coordinate. Using a error of 1.0 to compensate.
const error = 1.0;
const actualDistance = haversine(
latitudeFloat1,
longitudeFloat1,
latitudeFloat2,
longitudeFloat2,
latitude1,
longitude1,
latitude2,
longitude2,
isMetric
);
expect(actualDistance, `${i}`).toBeLessThanOrEqual(radius);
expect(actualDistance).toBeLessThanOrEqual(radius);
}
});
});
Expand Down

0 comments on commit f380b82

Please sign in to comment.