Skip to content

Commit d07d96d

Browse files
authored
feat(location): simple coordinate methods (#3528)
1 parent f08b243 commit d07d96d

File tree

4 files changed

+238
-221
lines changed

4 files changed

+238
-221
lines changed

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export type { HelpersModule, SimpleHelpersModule } from './modules/helpers';
6969
export type { ImageModule } from './modules/image';
7070
export { IPv4Network } from './modules/internet';
7171
export type { IPv4NetworkType, InternetModule } from './modules/internet';
72-
export type { LocationModule } from './modules/location';
72+
export type { LocationModule, SimpleLocationModule } from './modules/location';
7373
export type { LoremModule } from './modules/lorem';
7474
export type { MusicModule } from './modules/music';
7575
export type { NumberModule } from './modules/number';

src/modules/location/index.ts

Lines changed: 179 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import type { Faker } from '../..';
12
import { FakerError } from '../../errors/faker-error';
2-
import { ModuleBase } from '../../internal/module-base';
3+
import { SimpleModuleBase } from '../../internal/module-base';
34

45
/**
56
* Represents a language with its full name, 2 character ISO 639-1 code, and 3 character ISO 639-2 code.
@@ -21,6 +22,178 @@ export interface Language {
2122
alpha3: string;
2223
}
2324

25+
/**
26+
* Module with location functions that don't require localized data
27+
*/
28+
export class SimpleLocationModule extends SimpleModuleBase {
29+
/**
30+
* Generates a random latitude.
31+
*
32+
* @param options An options object.
33+
* @param options.max The upper bound for the latitude to generate. Defaults to `90`.
34+
* @param options.min The lower bound for the latitude to generate. Defaults to `-90`.
35+
* @param options.precision The number of decimal points of precision for the latitude. Defaults to `4`.
36+
*
37+
* @example
38+
* faker.location.latitude() // -30.9501
39+
* faker.location.latitude({ max: 10 }) // 5.7225
40+
* faker.location.latitude({ max: 10, min: -10 }) // -9.6273
41+
* faker.location.latitude({ max: 10, min: -10, precision: 5 }) // 2.68452
42+
*
43+
* @since 8.0.0
44+
*/
45+
latitude(
46+
options: {
47+
/**
48+
* The upper bound for the latitude to generate.
49+
*
50+
* @default 90
51+
*/
52+
max?: number;
53+
/**
54+
* The lower bound for the latitude to generate.
55+
*
56+
* @default -90
57+
*/
58+
min?: number;
59+
/**
60+
* The number of decimal points of precision for the latitude.
61+
*
62+
* @default 4
63+
*/
64+
precision?: number;
65+
} = {}
66+
): number {
67+
const { max = 90, min = -90, precision = 4 } = options;
68+
69+
return this.faker.number.float({ min, max, fractionDigits: precision });
70+
}
71+
72+
/**
73+
* Generates a random longitude.
74+
*
75+
* @param options An options object.
76+
* @param options.max The upper bound for the longitude to generate. Defaults to `180`.
77+
* @param options.min The lower bound for the longitude to generate. Defaults to `-180`.
78+
* @param options.precision The number of decimal points of precision for the longitude. Defaults to `4`.
79+
*
80+
* @example
81+
* faker.location.longitude() // -30.9501
82+
* faker.location.longitude({ max: 10 }) // 5.7225
83+
* faker.location.longitude({ max: 10, min: -10 }) // -9.6273
84+
* faker.location.longitude({ max: 10, min: -10, precision: 5 }) // 2.68452
85+
*
86+
* @since 8.0.0
87+
*/
88+
longitude(
89+
options: {
90+
/**
91+
* The upper bound for the longitude to generate.
92+
*
93+
* @default 180
94+
*/
95+
max?: number;
96+
/**
97+
* The lower bound for the longitude to generate.
98+
*
99+
* @default -180
100+
*/
101+
min?: number;
102+
/**
103+
* The number of decimal points of precision for the longitude.
104+
*
105+
* @default 4
106+
*/
107+
precision?: number;
108+
} = {}
109+
): number {
110+
const { max = 180, min = -180, precision = 4 } = options;
111+
112+
return this.faker.number.float({ max, min, fractionDigits: precision });
113+
}
114+
115+
/**
116+
* Generates a random GPS coordinate within the specified radius from the given coordinate.
117+
*
118+
* @param options The options for generating a GPS coordinate.
119+
* @param options.origin The original coordinate to get a new coordinate close to.
120+
* If no coordinate is given, a random one will be chosen.
121+
* @param options.radius The maximum distance from the given coordinate to the new coordinate. Defaults to `10`.
122+
* @param options.isMetric If `true` assume the radius to be in kilometers. If `false` for miles. Defaults to `false`.
123+
*
124+
* @example
125+
* faker.location.nearbyGPSCoordinate() // [ 33.8475, -170.5953 ]
126+
* faker.location.nearbyGPSCoordinate({ origin: [33, -170] }) // [ 33.0165, -170.0636 ]
127+
* faker.location.nearbyGPSCoordinate({ origin: [33, -170], radius: 1000, isMetric: true }) // [ 37.9163, -179.2408 ]
128+
*
129+
* @since 8.0.0
130+
*/
131+
nearbyGPSCoordinate(
132+
options: {
133+
/**
134+
* The original coordinate to get a new coordinate close to.
135+
*/
136+
origin?: [latitude: number, longitude: number];
137+
/**
138+
* The maximum distance from the given coordinate to the new coordinate.
139+
*
140+
* @default 10
141+
*/
142+
radius?: number;
143+
/**
144+
* If `true` assume the radius to be in kilometers. If `false` for miles.
145+
*
146+
* @default false
147+
*/
148+
isMetric?: boolean;
149+
} = {}
150+
): [latitude: number, longitude: number] {
151+
const { origin, radius = 10, isMetric = false } = options;
152+
153+
// If there is no origin, the best we can do is return a random GPS coordinate.
154+
if (origin == null) {
155+
return [this.latitude(), this.longitude()];
156+
}
157+
158+
const angleRadians = this.faker.number.float({
159+
max: 2 * Math.PI,
160+
fractionDigits: 5,
161+
}); // in ° radians
162+
163+
const radiusMetric = isMetric ? radius : radius * 1.60934; // in km
164+
const errorCorrection = 0.995; // avoid float issues
165+
const distanceInKm =
166+
this.faker.number.float({
167+
max: radiusMetric,
168+
fractionDigits: 3,
169+
}) * errorCorrection; // in km
170+
171+
/**
172+
* The distance in km per degree for earth.
173+
*/
174+
const kmPerDegree = 40_000 / 360; // in km/°
175+
176+
const distanceInDegree = distanceInKm / kmPerDegree; // in °
177+
178+
const coordinate: [latitude: number, longitude: number] = [
179+
origin[0] + Math.sin(angleRadians) * distanceInDegree,
180+
origin[1] + Math.cos(angleRadians) * distanceInDegree,
181+
];
182+
183+
// Box latitude [-90°, 90°]
184+
coordinate[0] = coordinate[0] % 180;
185+
if (coordinate[0] < -90 || coordinate[0] > 90) {
186+
coordinate[0] = Math.sign(coordinate[0]) * 180 - coordinate[0];
187+
coordinate[1] += 180;
188+
}
189+
190+
// Box longitude [-180°, 180°]
191+
coordinate[1] = (((coordinate[1] % 360) + 540) % 360) - 180;
192+
193+
return [coordinate[0], coordinate[1]];
194+
}
195+
}
196+
24197
/**
25198
* Module to generate addresses and locations. Prior to Faker 8.0.0, this module was known as `faker.address`.
26199
*
@@ -32,7 +205,11 @@ export interface Language {
32205
*
33206
* For a random country, you can use [`country()`](https://fakerjs.dev/api/location.html#country) or [`countryCode()`](https://fakerjs.dev/api/location.html#countrycode).
34207
*/
35-
export class LocationModule extends ModuleBase {
208+
export class LocationModule extends SimpleLocationModule {
209+
constructor(protected readonly faker: Faker) {
210+
super(faker);
211+
}
212+
36213
/**
37214
* Generates random zip code from specified format. If format is not specified,
38215
* the locale's zip format is used.
@@ -350,92 +527,6 @@ export class LocationModule extends ModuleBase {
350527
return this.faker.helpers.arrayElement(stateDataSet);
351528
}
352529

353-
/**
354-
* Generates a random latitude.
355-
*
356-
* @param options An options object.
357-
* @param options.max The upper bound for the latitude to generate. Defaults to `90`.
358-
* @param options.min The lower bound for the latitude to generate. Defaults to `-90`.
359-
* @param options.precision The number of decimal points of precision for the latitude. Defaults to `4`.
360-
*
361-
* @example
362-
* faker.location.latitude() // -30.9501
363-
* faker.location.latitude({ max: 10 }) // 5.7225
364-
* faker.location.latitude({ max: 10, min: -10 }) // -9.6273
365-
* faker.location.latitude({ max: 10, min: -10, precision: 5 }) // 2.68452
366-
*
367-
* @since 8.0.0
368-
*/
369-
latitude(
370-
options: {
371-
/**
372-
* The upper bound for the latitude to generate.
373-
*
374-
* @default 90
375-
*/
376-
max?: number;
377-
/**
378-
* The lower bound for the latitude to generate.
379-
*
380-
* @default -90
381-
*/
382-
min?: number;
383-
/**
384-
* The number of decimal points of precision for the latitude.
385-
*
386-
* @default 4
387-
*/
388-
precision?: number;
389-
} = {}
390-
): number {
391-
const { max = 90, min = -90, precision = 4 } = options;
392-
393-
return this.faker.number.float({ min, max, fractionDigits: precision });
394-
}
395-
396-
/**
397-
* Generates a random longitude.
398-
*
399-
* @param options An options object.
400-
* @param options.max The upper bound for the longitude to generate. Defaults to `180`.
401-
* @param options.min The lower bound for the longitude to generate. Defaults to `-180`.
402-
* @param options.precision The number of decimal points of precision for the longitude. Defaults to `4`.
403-
*
404-
* @example
405-
* faker.location.longitude() // -30.9501
406-
* faker.location.longitude({ max: 10 }) // 5.7225
407-
* faker.location.longitude({ max: 10, min: -10 }) // -9.6273
408-
* faker.location.longitude({ max: 10, min: -10, precision: 5 }) // 2.68452
409-
*
410-
* @since 8.0.0
411-
*/
412-
longitude(
413-
options: {
414-
/**
415-
* The upper bound for the longitude to generate.
416-
*
417-
* @default 180
418-
*/
419-
max?: number;
420-
/**
421-
* The lower bound for the longitude to generate.
422-
*
423-
* @default -180
424-
*/
425-
min?: number;
426-
/**
427-
* The number of decimal points of precision for the longitude.
428-
*
429-
* @default 4
430-
*/
431-
precision?: number;
432-
} = {}
433-
): number {
434-
const { max = 180, min = -180, precision = 4 } = options;
435-
436-
return this.faker.number.float({ max, min, fractionDigits: precision });
437-
}
438-
439530
/**
440531
* Returns a random direction (cardinal and ordinal; northwest, east, etc).
441532
*
@@ -549,87 +640,6 @@ export class LocationModule extends ModuleBase {
549640
);
550641
}
551642

552-
/**
553-
* Generates a random GPS coordinate within the specified radius from the given coordinate.
554-
*
555-
* @param options The options for generating a GPS coordinate.
556-
* @param options.origin The original coordinate to get a new coordinate close to.
557-
* If no coordinate is given, a random one will be chosen.
558-
* @param options.radius The maximum distance from the given coordinate to the new coordinate. Defaults to `10`.
559-
* @param options.isMetric If `true` assume the radius to be in kilometers. If `false` for miles. Defaults to `false`.
560-
*
561-
* @example
562-
* faker.location.nearbyGPSCoordinate() // [ 33.8475, -170.5953 ]
563-
* faker.location.nearbyGPSCoordinate({ origin: [33, -170] }) // [ 33.0165, -170.0636 ]
564-
* faker.location.nearbyGPSCoordinate({ origin: [33, -170], radius: 1000, isMetric: true }) // [ 37.9163, -179.2408 ]
565-
*
566-
* @since 8.0.0
567-
*/
568-
nearbyGPSCoordinate(
569-
options: {
570-
/**
571-
* The original coordinate to get a new coordinate close to.
572-
*/
573-
origin?: [latitude: number, longitude: number];
574-
/**
575-
* The maximum distance from the given coordinate to the new coordinate.
576-
*
577-
* @default 10
578-
*/
579-
radius?: number;
580-
/**
581-
* If `true` assume the radius to be in kilometers. If `false` for miles.
582-
*
583-
* @default false
584-
*/
585-
isMetric?: boolean;
586-
} = {}
587-
): [latitude: number, longitude: number] {
588-
const { origin, radius = 10, isMetric = false } = options;
589-
590-
// If there is no origin, the best we can do is return a random GPS coordinate.
591-
if (origin == null) {
592-
return [this.latitude(), this.longitude()];
593-
}
594-
595-
const angleRadians = this.faker.number.float({
596-
max: 2 * Math.PI,
597-
fractionDigits: 5,
598-
}); // in ° radians
599-
600-
const radiusMetric = isMetric ? radius : radius * 1.60934; // in km
601-
const errorCorrection = 0.995; // avoid float issues
602-
const distanceInKm =
603-
this.faker.number.float({
604-
max: radiusMetric,
605-
fractionDigits: 3,
606-
}) * errorCorrection; // in km
607-
608-
/**
609-
* The distance in km per degree for earth.
610-
*/
611-
const kmPerDegree = 40_000 / 360; // in km/°
612-
613-
const distanceInDegree = distanceInKm / kmPerDegree; // in °
614-
615-
const coordinate: [latitude: number, longitude: number] = [
616-
origin[0] + Math.sin(angleRadians) * distanceInDegree,
617-
origin[1] + Math.cos(angleRadians) * distanceInDegree,
618-
];
619-
620-
// Box latitude [-90°, 90°]
621-
coordinate[0] = coordinate[0] % 180;
622-
if (coordinate[0] < -90 || coordinate[0] > 90) {
623-
coordinate[0] = Math.sign(coordinate[0]) * 180 - coordinate[0];
624-
coordinate[1] += 180;
625-
}
626-
627-
// Box longitude [-180°, 180°]
628-
coordinate[1] = (((coordinate[1] % 360) + 540) % 360) - 180;
629-
630-
return [coordinate[0], coordinate[1]];
631-
}
632-
633643
/**
634644
* Returns a random IANA time zone relevant to this locale.
635645
*

0 commit comments

Comments
 (0)