From 1693192ae6fd0456a759a00fbd822162c43eba87 Mon Sep 17 00:00:00 2001 From: Shinigami92 Date: Tue, 26 Apr 2022 14:31:10 +0200 Subject: [PATCH 01/11] fix: address.nearbyGPSCoordinate --- src/address.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/address.ts b/src/address.ts index e1518a465db..833ed1c51a8 100644 --- a/src/address.ts +++ b/src/address.ts @@ -41,22 +41,22 @@ function coordinateWithOffset( distance: number, isMetric: boolean ): [latitude: number, longitude: number] { - const R = 6378.137; // Radius of the Earth (http://nssdc.gsfc.nasa.gov/planetary/factsheet/earthfact.html) + const EARTH_RADIUS = 6378.137; // http://nssdc.gsfc.nasa.gov/planetary/factsheet/earthfact.html const d = isMetric ? distance : kilometersToMiles(distance); // Distance in km - const lat1 = degreesToRadians(coordinate[0]); //Current lat point converted to radians - const lon1 = degreesToRadians(coordinate[1]); //Current long point converted to radians + const lat1 = degreesToRadians(coordinate[0]); // Current lat point converted to radians + const lon1 = degreesToRadians(coordinate[1]); // Current long point converted to radians const lat2 = Math.asin( - Math.sin(lat1) * Math.cos(d / R) + - Math.cos(lat1) * Math.sin(d / R) * Math.cos(bearing) + Math.sin(lat1) * Math.cos(d / EARTH_RADIUS) + + Math.cos(lat1) * Math.sin(d / EARTH_RADIUS) * Math.cos(bearing) ); let lon2 = lon1 + Math.atan2( - Math.sin(bearing) * Math.sin(d / R) * Math.cos(lat1), - Math.cos(d / R) - Math.sin(lat1) * Math.sin(lat2) + Math.sin(bearing) * Math.sin(d / EARTH_RADIUS) * Math.cos(lat1), + Math.cos(d / EARTH_RADIUS) - Math.sin(lat1) * Math.sin(lat2) ); // Keep longitude in range [-180, 180] From 087dc3f8ebe1abfe0ae6db46c3a30afd0577586f Mon Sep 17 00:00:00 2001 From: Shinigami92 Date: Tue, 26 Apr 2022 14:35:02 +0200 Subject: [PATCH 02/11] chore: rename constant --- src/address.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/address.ts b/src/address.ts index 833ed1c51a8..e9f9a4f51d4 100644 --- a/src/address.ts +++ b/src/address.ts @@ -41,22 +41,24 @@ function coordinateWithOffset( distance: number, isMetric: boolean ): [latitude: number, longitude: number] { - const EARTH_RADIUS = 6378.137; // http://nssdc.gsfc.nasa.gov/planetary/factsheet/earthfact.html + 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 lat1 = degreesToRadians(coordinate[0]); // Current lat point converted to radians const lon1 = degreesToRadians(coordinate[1]); // Current long point converted to radians const lat2 = Math.asin( - Math.sin(lat1) * Math.cos(d / EARTH_RADIUS) + - Math.cos(lat1) * Math.sin(d / EARTH_RADIUS) * Math.cos(bearing) + Math.sin(lat1) * Math.cos(d / EQUATORIAL_EARTH_RADIUS) + + Math.cos(lat1) * Math.sin(d / EQUATORIAL_EARTH_RADIUS) * Math.cos(bearing) ); let lon2 = lon1 + Math.atan2( - Math.sin(bearing) * Math.sin(d / EARTH_RADIUS) * Math.cos(lat1), - Math.cos(d / EARTH_RADIUS) - Math.sin(lat1) * Math.sin(lat2) + Math.sin(bearing) * + Math.sin(d / EQUATORIAL_EARTH_RADIUS) * + Math.cos(lat1), + Math.cos(d / EQUATORIAL_EARTH_RADIUS) - Math.sin(lat1) * Math.sin(lat2) ); // Keep longitude in range [-180, 180] From 8bc9b87f69bb74071cf47b57c5c68fdd993c8633 Mon Sep 17 00:00:00 2001 From: Shinigami92 Date: Tue, 26 Apr 2022 14:45:38 +0200 Subject: [PATCH 03/11] chore: rename variables --- src/address.ts | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/address.ts b/src/address.ts index e9f9a4f51d4..382ee5beef5 100644 --- a/src/address.ts +++ b/src/address.ts @@ -44,31 +44,34 @@ function coordinateWithOffset( 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 lat1 = degreesToRadians(coordinate[0]); // Current lat point converted to radians - const lon1 = degreesToRadians(coordinate[1]); // Current long point converted to radians + const latitudeCenter = degreesToRadians(coordinate[0]); // Current latitude point converted to radians + const longitudeCenter = degreesToRadians(coordinate[1]); // Current longitude point converted to radians - const lat2 = Math.asin( - Math.sin(lat1) * Math.cos(d / EQUATORIAL_EARTH_RADIUS) + - Math.cos(lat1) * Math.sin(d / EQUATORIAL_EARTH_RADIUS) * Math.cos(bearing) + 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 lon2 = - lon1 + + let longitudeOffset = + longitudeCenter + Math.atan2( Math.sin(bearing) * Math.sin(d / EQUATORIAL_EARTH_RADIUS) * - Math.cos(lat1), - Math.cos(d / EQUATORIAL_EARTH_RADIUS) - Math.sin(lat1) * Math.sin(lat2) + Math.cos(latitudeCenter), + Math.cos(d / EQUATORIAL_EARTH_RADIUS) - + Math.sin(latitudeCenter) * Math.sin(latitudeOffset) ); // Keep longitude in range [-180, 180] - if (lon2 > degreesToRadians(180)) { - lon2 = lon2 - degreesToRadians(360); - } else if (lon2 < degreesToRadians(-180)) { - lon2 = lon2 + degreesToRadians(360); + if (longitudeOffset > degreesToRadians(180)) { + longitudeOffset = longitudeOffset - degreesToRadians(360); + } else if (longitudeOffset < degreesToRadians(-180)) { + longitudeOffset = longitudeOffset + degreesToRadians(360); } - return [radiansToDegrees(lat2), radiansToDegrees(lon2)]; + return [radiansToDegrees(latitudeOffset), radiansToDegrees(longitudeOffset)]; } /** From 195dfef6155d10edf89b6024e112687875cd0122 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Tue, 26 Apr 2022 16:10:04 +0200 Subject: [PATCH 04/11] chore: checkpoint --- src/address.ts | 71 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/src/address.ts b/src/address.ts index 382ee5beef5..06bfa80df2a 100644 --- a/src/address.ts +++ b/src/address.ts @@ -498,34 +498,61 @@ export class Address { // TODO ST-DDT 2022-02-10: Allow coordinate parameter to be [string, string]. nearbyGPSCoordinate( coordinate?: [latitude: number, longitude: number], - radius?: number, - isMetric?: boolean + radius: number = 10, + isMetric: boolean = false ): [latitude: string, longitude: string] { // If there is no coordinate, the best we can do is return a random GPS coordinate. if (coordinate === undefined) { return [this.latitude(), this.longitude()]; } - radius = radius || 10.0; - isMetric = isMetric || false; - - // TODO: implement either a gaussian/uniform distribution of points in circular region. - // Possibly include param to function that allows user to choose between distributions. - - // This approach will likely result in a higher density of points near the center. - const randomCoord = coordinateWithOffset( - coordinate, - degreesToRadians( - this.faker.datatype.number({ - min: 0, - max: 360, - precision: 1e-4, - }) - ), - radius, - isMetric - ); - return [randomCoord[0].toFixed(4), randomCoord[1].toFixed(4)]; + const twoPi = 2 * Math.PI; + const angle = this.faker.datatype.float({ + min: 0, + max: twoPi, + precision: 0.00001, + }); + + const earthRadius = 40000; // in km + const radiusMetric = isMetric ? radius : radius * 1.60934; // in km + + const kmPerDegreeLatitude = earthRadius / 360; // in km/° + const maxDistanceLatitude = radiusMetric / kmPerDegreeLatitude; // in ° + const distanceLatitude = this.faker.datatype.float({ + min: 0, + max: maxDistanceLatitude, + precision: 0.00001, + }); // in ° + + const newLatitude = coordinate[0] + Math.cos(angle) * distanceLatitude; + + const earthRadiusAtLatitude = + Math.cos((newLatitude / 360) * twoPi) * earthRadius; + const kmPerDegreeLongitude = earthRadiusAtLatitude / 360; // in km/° + const maxDistanceLongitude = radiusMetric / kmPerDegreeLongitude; // in ° + const distanceLongitude = this.faker.datatype.float({ + min: 0, + max: maxDistanceLongitude, + precision: 0.00001, + }); // in ° + + const newLongitude = coordinate[1] + Math.sin(angle) * distanceLongitude; + + const newCoordinate: [latitude: number, longitude: number] = [ + newLatitude, + newLongitude, + ]; + + // Box latitude [-90°, 90°] + newCoordinate[0] = newCoordinate[0] % 180; + if (newCoordinate[0] < -90 || newCoordinate[0] > 90) { + newCoordinate[0] = Math.sign(newCoordinate[0]) * 180 - newCoordinate[0]; + newCoordinate[1] += 180; + } + // Box longitude [-180°, 180°] + newCoordinate[1] = (((newCoordinate[1] % 360) + 180) % 360) - 180; + + return [newCoordinate[0].toFixed(4), newCoordinate[1].toFixed(4)]; } /** From 52b0a05a2a901fff243c6446bad15e36d4c0d2e3 Mon Sep 17 00:00:00 2001 From: Shinigami92 Date: Tue, 26 Apr 2022 16:18:43 +0200 Subject: [PATCH 05/11] test: rename variables --- test/address.spec.ts | 63 +++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/test/address.spec.ts b/test/address.spec.ts index 0d7417c27bc..214f38a3df8 100644 --- a/test/address.spec.ts +++ b/test/address.spec.ts @@ -556,39 +556,48 @@ describe('address', () => { describe('nearbyGPSCoordinate()', () => { it('should return random gps coordinate within a distance of another one', () => { - function haversine(lat1, lon1, lat2, lon2, isMetric) { - function degreesToRadians(degrees) { + function haversine( + latitude1: number, + longitude1: number, + latitude2: number, + longitude2: number, + isMetric: boolean + ) { + function degreesToRadians(degrees: number) { return degrees * (Math.PI / 180.0); } - function kilometersToMiles(miles) { + function kilometersToMiles(miles: number) { return miles * 0.621371; } - const R = 6378.137; - const dLat = degreesToRadians(lat2 - lat1); - const dLon = degreesToRadians(lon2 - lon1); + const EQUATORIAL_EARTH_RADIUS = 6378.137; + const distanceLatitude = degreesToRadians(latitude2 - latitude1); + const distanceLongitude = degreesToRadians(longitude2 - longitude1); const a = - Math.sin(dLat / 2) * Math.sin(dLat / 2) + - Math.cos(degreesToRadians(lat1)) * - Math.cos(degreesToRadians(lat2)) * - Math.sin(dLon / 2) * - Math.sin(dLon / 2); - const distance = R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + Math.sin(distanceLatitude / 2) * Math.sin(distanceLatitude / 2) + + Math.cos(degreesToRadians(latitude1)) * + Math.cos(degreesToRadians(latitude2)) * + Math.sin(distanceLongitude / 2) * + Math.sin(distanceLongitude / 2); + const distance = + EQUATORIAL_EARTH_RADIUS * + 2 * + Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return isMetric ? distance : kilometersToMiles(distance); } - let latFloat1: number; - let lonFloat1: number; + let latitudeFloat1: number; + let longitudeFloat1: number; let isMetric: boolean; for (let i = 0; i < 10000; i++) { - latFloat1 = parseFloat(faker.address.latitude()); - lonFloat1 = parseFloat(faker.address.longitude()); + latitudeFloat1 = parseFloat(faker.address.latitude()); + longitudeFloat1 = parseFloat(faker.address.longitude()); const radius = Math.random() * 99 + 1; // range of [1, 100) isMetric = Math.round(Math.random()) === 1; const coordinate = faker.address.nearbyGPSCoordinate( - [latFloat1, lonFloat1], + [latitudeFloat1, longitudeFloat1], radius, isMetric ); @@ -597,23 +606,23 @@ describe('address', () => { expect(coordinate[0]).toBeTypeOf('string'); expect(coordinate[1]).toBeTypeOf('string'); - const latFloat2 = parseFloat(coordinate[0]); - expect(latFloat2).toBeGreaterThanOrEqual(-90.0); - expect(latFloat2).toBeLessThanOrEqual(90.0); + const latitudeFloat2 = parseFloat(coordinate[0]); + expect(latitudeFloat2).toBeGreaterThanOrEqual(-90.0); + expect(latitudeFloat2).toBeLessThanOrEqual(90.0); - const lonFloat2 = parseFloat(coordinate[1]); - expect(lonFloat2).toBeGreaterThanOrEqual(-180.0); - expect(lonFloat2).toBeLessThanOrEqual(180.0); + const longitudeFloat2 = parseFloat(coordinate[1]); + expect(longitudeFloat2).toBeGreaterThanOrEqual(-180.0); + expect(longitudeFloat2).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( - latFloat1, - lonFloat1, - latFloat2, - lonFloat2, + latitudeFloat1, + longitudeFloat1, + latitudeFloat2, + longitudeFloat2, isMetric ); expect(actualDistance).toBeLessThanOrEqual(radius + error); From 36e9b79530c9b4d7d43a169a7cb629c9885e6ed0 Mon Sep 17 00:00:00 2001 From: Shinigami92 Date: Tue, 26 Apr 2022 16:25:52 +0200 Subject: [PATCH 06/11] chore: extract functions out of test run --- test/address.spec.ts | 60 ++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/test/address.spec.ts b/test/address.spec.ts index 214f38a3df8..652c33e25b3 100644 --- a/test/address.spec.ts +++ b/test/address.spec.ts @@ -1,6 +1,36 @@ import { afterEach, describe, expect, it } from 'vitest'; import { faker } from '../src'; +function degreesToRadians(degrees: number) { + return degrees * (Math.PI / 180.0); +} + +function kilometersToMiles(miles: number) { + return miles * 0.621371; +} + +function haversine( + latitude1: number, + longitude1: number, + latitude2: number, + longitude2: number, + isMetric: boolean +) { + const EQUATORIAL_EARTH_RADIUS = 6378.137; + const distanceLatitude = degreesToRadians(latitude2 - latitude1); + const distanceLongitude = degreesToRadians(longitude2 - longitude1); + const a = + Math.sin(distanceLatitude / 2) * Math.sin(distanceLatitude / 2) + + Math.cos(degreesToRadians(latitude1)) * + Math.cos(degreesToRadians(latitude2)) * + Math.sin(distanceLongitude / 2) * + Math.sin(distanceLongitude / 2); + const distance = + EQUATORIAL_EARTH_RADIUS * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + return isMetric ? distance : kilometersToMiles(distance); +} + const seededRuns = [ { seed: 42, @@ -556,36 +586,6 @@ describe('address', () => { describe('nearbyGPSCoordinate()', () => { it('should return random gps coordinate within a distance of another one', () => { - function haversine( - latitude1: number, - longitude1: number, - latitude2: number, - longitude2: number, - isMetric: boolean - ) { - function degreesToRadians(degrees: number) { - return degrees * (Math.PI / 180.0); - } - function kilometersToMiles(miles: number) { - return miles * 0.621371; - } - const EQUATORIAL_EARTH_RADIUS = 6378.137; - const distanceLatitude = degreesToRadians(latitude2 - latitude1); - const distanceLongitude = degreesToRadians(longitude2 - longitude1); - const a = - Math.sin(distanceLatitude / 2) * Math.sin(distanceLatitude / 2) + - Math.cos(degreesToRadians(latitude1)) * - Math.cos(degreesToRadians(latitude2)) * - Math.sin(distanceLongitude / 2) * - Math.sin(distanceLongitude / 2); - const distance = - EQUATORIAL_EARTH_RADIUS * - 2 * - Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - - return isMetric ? distance : kilometersToMiles(distance); - } - let latitudeFloat1: number; let longitudeFloat1: number; let isMetric: boolean; From ab88a3f39f885c45dcbff056df5c1c5bf82e8933 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Tue, 26 Apr 2022 18:19:43 +0200 Subject: [PATCH 07/11] chore: checkpoint 2 --- src/address.ts | 44 ++++++++++++------------- test/address.spec.ts | 76 +++++++------------------------------------- 2 files changed, 34 insertions(+), 86 deletions(-) diff --git a/src/address.ts b/src/address.ts index 06bfa80df2a..88d44dd113c 100644 --- a/src/address.ts +++ b/src/address.ts @@ -507,36 +507,36 @@ export class Address { } const twoPi = 2 * Math.PI; - const angle = this.faker.datatype.float({ + const angleRadians = Math.PI / 4; /* this.faker.datatype.float({ min: 0, max: twoPi, precision: 0.00001, - }); + }); // in ° radians*/ - const earthRadius = 40000; // in km const radiusMetric = isMetric ? radius : radius * 1.60934; // in km - - const kmPerDegreeLatitude = earthRadius / 360; // in km/° - const maxDistanceLatitude = radiusMetric / kmPerDegreeLatitude; // in ° - const distanceLatitude = this.faker.datatype.float({ + const totalDistance = 100 * 0.99; /*this.faker.datatype.float({ min: 0, - max: maxDistanceLatitude, - precision: 0.00001, - }); // in ° + max: radiusMetric, + precision: 0.001, + }); // in km*/ - const newLatitude = coordinate[0] + Math.cos(angle) * distanceLatitude; + const kmPerDegreeLatitude = 111; // in km/° + const distanceLatitude = totalDistance / kmPerDegreeLatitude; // in ° - const earthRadiusAtLatitude = - Math.cos((newLatitude / 360) * twoPi) * earthRadius; - const kmPerDegreeLongitude = earthRadiusAtLatitude / 360; // in km/° - const maxDistanceLongitude = radiusMetric / kmPerDegreeLongitude; // in ° - const distanceLongitude = this.faker.datatype.float({ - min: 0, - max: maxDistanceLongitude, - precision: 0.00001, - }); // in ° + const offsetLatitude = Math.sin(angleRadians) * distanceLatitude; + 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 newLongitude = coordinate[1] + Math.sin(angle) * distanceLongitude; + const offsetLongitude = Math.cos(angleRadians) * distanceLongitude; + const newLongitude = coordinate[1] + offsetLongitude; const newCoordinate: [latitude: number, longitude: number] = [ newLatitude, @@ -550,7 +550,7 @@ export class Address { newCoordinate[1] += 180; } // Box longitude [-180°, 180°] - newCoordinate[1] = (((newCoordinate[1] % 360) + 180) % 360) - 180; + newCoordinate[1] = (((newCoordinate[1] % 360) + 540) % 360) - 180; return [newCoordinate[0].toFixed(4), newCoordinate[1].toFixed(4)]; } diff --git a/test/address.spec.ts b/test/address.spec.ts index 652c33e25b3..c155df81c8d 100644 --- a/test/address.spec.ts +++ b/test/address.spec.ts @@ -59,7 +59,7 @@ const seededRuns = [ cardinalDirection: 'East', cardinalDirectionAbbr: 'E', timeZone: 'Europe/Amsterdam', - nearbyGpsCoordinates: ['-0.0394', '0.0396'], + nearbyGpsCoordinates: ['-0.0811', '0.0816'], }, }, { @@ -89,7 +89,7 @@ const seededRuns = [ cardinalDirection: 'East', cardinalDirectionAbbr: 'E', timeZone: 'Africa/Casablanca', - nearbyGpsCoordinates: ['-0.0042', '0.0557'], + nearbyGpsCoordinates: ['-0.0061', '0.0808'], }, }, { @@ -119,12 +119,12 @@ const seededRuns = [ cardinalDirection: 'West', cardinalDirectionAbbr: 'W', timeZone: 'Asia/Magadan', - nearbyGpsCoordinates: ['0.0503', '-0.0242'], + nearbyGpsCoordinates: ['0.0597', '-0.0288'], }, }, ]; -const NON_SEEDED_BASED_RUN = 5; +const NON_SEEDED_BASED_RUN = 1; describe('address', () => { afterEach(() => { @@ -584,17 +584,14 @@ describe('address', () => { }); }); - describe('nearbyGPSCoordinate()', () => { + describe.only('nearbyGPSCoordinate()', () => { it('should return random gps coordinate within a distance of another one', () => { - let latitudeFloat1: number; - let longitudeFloat1: number; - let isMetric: boolean; - - for (let i = 0; i < 10000; i++) { - latitudeFloat1 = parseFloat(faker.address.latitude()); - longitudeFloat1 = parseFloat(faker.address.longitude()); - const radius = Math.random() * 99 + 1; // range of [1, 100) - isMetric = Math.round(Math.random()) === 1; + 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(); const coordinate = faker.address.nearbyGPSCoordinate( [latitudeFloat1, longitudeFloat1], @@ -625,58 +622,9 @@ describe('address', () => { longitudeFloat2, isMetric ); - expect(actualDistance).toBeLessThanOrEqual(radius + error); + expect(actualDistance, `${i}`).toBeLessThanOrEqual(radius); } }); - - it('should return near metric coordinates when radius is undefined', () => { - const latitude = parseFloat(faker.address.latitude()); - const longitude = parseFloat(faker.address.longitude()); - const isMetric = true; - - const coordinate = faker.address.nearbyGPSCoordinate( - [latitude, longitude], - undefined, - isMetric - ); - - expect(coordinate.length).toBe(2); - expect(coordinate[0]).toBeTypeOf('string'); - expect(coordinate[1]).toBeTypeOf('string'); - - const distanceToTarget = - Math.pow(+coordinate[0] - latitude, 2) + - Math.pow(+coordinate[1] - longitude, 2); - - expect(distanceToTarget).toBeLessThanOrEqual( - 100 * 0.002 // 100 km ~= 0.9 degrees, we take 2 degrees - ); - }); - - it('should return near non metric coordinates when radius is undefined', () => { - const latitude = parseFloat(faker.address.latitude()); - const longitude = parseFloat(faker.address.longitude()); - const isMetric = false; - - const coordinate = faker.address.nearbyGPSCoordinate( - [latitude, longitude], - undefined, - isMetric - ); - - expect(coordinate.length).toBe(2); - expect(coordinate[0]).toBeTypeOf('string'); - expect(coordinate[1]).toBeTypeOf('string'); - - // const distanceToTarget = - // Math.pow(coordinate[0] - latitude, 2) + - // Math.pow(coordinate[1] - longitude, 2); - - // TODO @Shinigami92 2022-01-27: Investigate why this test sometimes fails - // expect(distanceToTarget).toBeLessThanOrEqual( - // 100 * 0.002 * 1.6093444978925633 // 100 miles to km ~= 0.9 degrees, we take 2 degrees - // ); - }); }); } }); From f380b82695b12c73ba652ec0984e339afa0a9312 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Tue, 26 Apr 2022 20:15:35 +0200 Subject: [PATCH 08/11] chore: simplify and fix impl --- src/address.ts | 108 ++++++------------------------------------- test/address.spec.ts | 47 +++++++++---------- 2 files changed, 36 insertions(+), 119 deletions(-) diff --git a/src/address.ts b/src/address.ts index 88d44dd113c..c05115a70e1 100644 --- a/src/address.ts +++ b/src/address.ts @@ -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. @@ -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] = [ diff --git a/test/address.spec.ts b/test/address.spec.ts index c155df81c8d..9b37636577c 100644 --- a/test/address.spec.ts +++ b/test/address.spec.ts @@ -59,7 +59,7 @@ const seededRuns = [ cardinalDirection: 'East', cardinalDirectionAbbr: 'E', timeZone: 'Europe/Amsterdam', - nearbyGpsCoordinates: ['-0.0811', '0.0816'], + nearbyGpsCoordinates: ['0.0814', '-0.0809'], }, }, { @@ -89,7 +89,7 @@ const seededRuns = [ cardinalDirection: 'East', cardinalDirectionAbbr: 'E', timeZone: 'Africa/Casablanca', - nearbyGpsCoordinates: ['-0.0061', '0.0808'], + nearbyGpsCoordinates: ['0.0806', '-0.0061'], }, }, { @@ -119,7 +119,7 @@ const seededRuns = [ cardinalDirection: 'West', cardinalDirectionAbbr: 'W', timeZone: 'Asia/Magadan', - nearbyGpsCoordinates: ['0.0597', '-0.0288'], + nearbyGpsCoordinates: ['-0.0287', '0.0596'], }, }, ]; @@ -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 ); @@ -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); } }); }); From d5420c6ab52160db026c38fe9e41ad2a6bb21390 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Tue, 26 Apr 2022 20:19:53 +0200 Subject: [PATCH 09/11] chore: simplify --- src/address.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/address.ts b/src/address.ts index c05115a70e1..40564f24b76 100644 --- a/src/address.ts +++ b/src/address.ts @@ -454,15 +454,9 @@ export class Address { const distanceInDegree = distanceInKm / kmPerDegree; // in ° - const offsetLatitude = Math.sin(angleRadians) * distanceInDegree; - const newLatitude = coordinate[0] + offsetLatitude; - - const offsetLongitude = Math.cos(angleRadians) * distanceInDegree; - const newLongitude = coordinate[1] + offsetLongitude; - const newCoordinate: [latitude: number, longitude: number] = [ - newLatitude, - newLongitude, + coordinate[0] + Math.sin(angleRadians) * distanceInDegree, + coordinate[1] + Math.cos(angleRadians) * distanceInDegree, ]; // Box latitude [-90°, 90°] From 4535a6af4c0fa768720231ac386a0a20d984845e Mon Sep 17 00:00:00 2001 From: Shinigami92 Date: Tue, 26 Apr 2022 21:01:29 +0200 Subject: [PATCH 10/11] chore: improvments --- src/address.ts | 11 +++--- test/address.spec.ts | 79 ++++++++++++++++++++++++-------------------- 2 files changed, 49 insertions(+), 41 deletions(-) diff --git a/src/address.ts b/src/address.ts index 40564f24b76..cfac28067ef 100644 --- a/src/address.ts +++ b/src/address.ts @@ -1,10 +1,5 @@ import type { Faker } from '.'; -/** - * The distance in km per degree. - */ -const kmPerDegree = 40_000 / 360; // in km/° - /** * Module to generate addresses and locations. */ @@ -452,6 +447,12 @@ export class Address { precision: 0.001, }) * errorCorrection; // in km + /** + * The distance in km per degree for earth. + */ + // TODO @Shinigami92 2022-04-26: Provide an option property to provide custom circumferences. + const kmPerDegree = 40_000 / 360; // in km/° + const distanceInDegree = distanceInKm / kmPerDegree; // in ° const newCoordinate: [latitude: number, longitude: number] = [ diff --git a/test/address.spec.ts b/test/address.spec.ts index 9b37636577c..ab852446187 100644 --- a/test/address.spec.ts +++ b/test/address.spec.ts @@ -1,5 +1,6 @@ import { afterEach, describe, expect, it } from 'vitest'; import { faker } from '../src'; +import { times } from './support/times'; function degreesToRadians(degrees: number) { return degrees * (Math.PI / 180.0); @@ -9,6 +10,9 @@ function kilometersToMiles(miles: number) { return miles * 0.621371; } +// http://nssdc.gsfc.nasa.gov/planetary/factsheet/earthfact.html +const EQUATORIAL_EARTH_RADIUS = 6378.137; + function haversine( latitude1: number, longitude1: number, @@ -16,7 +20,6 @@ function haversine( longitude2: number, isMetric: boolean ) { - const EQUATORIAL_EARTH_RADIUS = 6378.137; const distanceLatitude = degreesToRadians(latitude2 - latitude1); const distanceLongitude = degreesToRadians(longitude2 - longitude1); const a = @@ -124,7 +127,7 @@ const seededRuns = [ }, ]; -const NON_SEEDED_BASED_RUN = 1; +const NON_SEEDED_BASED_RUN = 5; describe('address', () => { afterEach(() => { @@ -585,41 +588,45 @@ describe('address', () => { }); describe('nearbyGPSCoordinate()', () => { - it('should return random gps coordinate within a distance of another one', () => { - 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( - [latitude1, longitude1], - radius, - isMetric - ); - - expect(coordinate.length).toBe(2); - expect(coordinate[0]).toBeTypeOf('string'); - expect(coordinate[1]).toBeTypeOf('string'); - - const latitude2 = +coordinate[0]; - expect(latitude2).toBeGreaterThanOrEqual(-90.0); - expect(latitude2).toBeLessThanOrEqual(90.0); - - const longitude2 = +coordinate[1]; - expect(longitude2).toBeGreaterThanOrEqual(-180.0); - expect(longitude2).toBeLessThanOrEqual(180.0); - - const actualDistance = haversine( - latitude1, - longitude1, - latitude2, - longitude2, - isMetric - ); - expect(actualDistance).toBeLessThanOrEqual(radius); + for (const isMetric of [true, false]) { + for (const radius of times(100)) { + it(`should return random gps coordinate within a distance of another one (${JSON.stringify( + { isMetric, radius } + )})`, () => { + for (let i = 0; i < 100; i++) { + const latitude1 = +faker.address.latitude(); + const longitude1 = +faker.address.longitude(); + + const coordinate = faker.address.nearbyGPSCoordinate( + [latitude1, longitude1], + radius, + isMetric + ); + + expect(coordinate.length).toBe(2); + expect(coordinate[0]).toBeTypeOf('string'); + expect(coordinate[1]).toBeTypeOf('string'); + + const latitude2 = +coordinate[0]; + expect(latitude2).toBeGreaterThanOrEqual(-90.0); + expect(latitude2).toBeLessThanOrEqual(90.0); + + const longitude2 = +coordinate[1]; + expect(longitude2).toBeGreaterThanOrEqual(-180.0); + expect(longitude2).toBeLessThanOrEqual(180.0); + + const actualDistance = haversine( + latitude1, + longitude1, + latitude2, + longitude2, + isMetric + ); + expect(actualDistance).toBeLessThanOrEqual(radius); + } + }); } - }); + } }); } }); From 7d1ed40cc981900fd3a39d95f0c9b400c11522ab Mon Sep 17 00:00:00 2001 From: Shinigami92 Date: Tue, 26 Apr 2022 21:59:46 +0200 Subject: [PATCH 11/11] test: apply suggestion --- test/address.spec.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/address.spec.ts b/test/address.spec.ts index ab852446187..a0fa2775319 100644 --- a/test/address.spec.ts +++ b/test/address.spec.ts @@ -590,10 +590,11 @@ describe('address', () => { describe('nearbyGPSCoordinate()', () => { for (const isMetric of [true, false]) { for (const radius of times(100)) { - it(`should return random gps coordinate within a distance of another one (${JSON.stringify( - { isMetric, radius } - )})`, () => { - for (let i = 0; i < 100; i++) { + it.each(times(5))( + `should return random gps coordinate within a distance of another one (${JSON.stringify( + { isMetric, radius } + )}) (iter: %s)`, + () => { const latitude1 = +faker.address.latitude(); const longitude1 = +faker.address.longitude(); @@ -624,7 +625,7 @@ describe('address', () => { ); expect(actualDistance).toBeLessThanOrEqual(radius); } - }); + ); } } });