@procedures/geo 0.2.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -0
- package/README.md +21 -2
- package/dist/.tsbuildinfo +1 -1
- package/dist/distance.d.ts +14 -0
- package/dist/distance.d.ts.map +1 -0
- package/dist/distance.js +20 -0
- package/dist/distance.js.map +1 -0
- package/dist/geo.d.ts +15 -0
- package/dist/geo.d.ts.map +1 -1
- package/dist/geo.js +5 -4
- package/dist/geo.js.map +1 -1
- package/dist/geohash.d.ts +30 -0
- package/dist/geohash.d.ts.map +1 -0
- package/dist/geohash.js +87 -0
- package/dist/geohash.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/procedures/geo/distance.d.ts +12 -0
- package/dist/procedures/geo/distance.d.ts.map +1 -0
- package/dist/procedures/geo/distance.js +14 -0
- package/dist/procedures/geo/distance.js.map +1 -0
- package/dist/procedures/geo/geohash.d.ts +11 -0
- package/dist/procedures/geo/geohash.d.ts.map +1 -0
- package/dist/procedures/geo/geohash.js +14 -0
- package/dist/procedures/geo/geohash.js.map +1 -0
- package/dist/procedures/geo/index.d.ts +3 -0
- package/dist/procedures/geo/index.d.ts.map +1 -0
- package/dist/procedures/geo/index.js +3 -0
- package/dist/procedures/geo/index.js.map +1 -0
- package/dist/procedures.d.ts +3 -1
- package/dist/procedures.d.ts.map +1 -1
- package/dist/procedures.js +34 -2
- package/dist/procedures.js.map +1 -1
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +29 -0
- package/dist/types.js.map +1 -0
- package/package.json +5 -3
- package/src/distance.ts +23 -0
- package/src/geo.ts +23 -7
- package/src/geohash.ts +97 -0
- package/src/index.ts +3 -0
- package/src/procedures/geo/distance.ts +16 -0
- package/src/procedures/geo/geohash.ts +16 -0
- package/src/procedures/geo/index.ts +2 -0
- package/src/procedures.ts +47 -2
- package/src/types.ts +67 -0
package/src/geo.ts
CHANGED
|
@@ -24,19 +24,34 @@ export const NOMINATIM_URL = "https://nominatim.openstreetmap.org";
|
|
|
24
24
|
/** The public OSRM demonstration server. It has a fair-use limit. A self-hosted OSRM has the same API. */
|
|
25
25
|
export const OSRM_URL = "https://router.project-osrm.org";
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* The part of `fetch` that this package uses. A host or a test can give its own function, for
|
|
29
|
+
* example a fetch with a cache or a recorder. The default is the global `fetch`.
|
|
30
|
+
*/
|
|
31
|
+
export type FetchJson = (
|
|
32
|
+
url: string,
|
|
33
|
+
init: { headers: Record<string, string>; signal: AbortSignal }
|
|
34
|
+
) => Promise<{ ok: boolean; status: number; json(): Promise<unknown> }>;
|
|
35
|
+
|
|
27
36
|
/** This function gets a JSON document, with a time limit and the signal of the call. */
|
|
28
37
|
export async function getJson(
|
|
29
38
|
url: string,
|
|
30
|
-
options: {
|
|
39
|
+
options: {
|
|
40
|
+
timeoutMs: number;
|
|
41
|
+
signal?: AbortSignal | undefined;
|
|
42
|
+
headers?: Record<string, string>;
|
|
43
|
+
fetch?: FetchJson | undefined;
|
|
44
|
+
},
|
|
31
45
|
service: string
|
|
32
46
|
): Promise<unknown> {
|
|
33
47
|
const signals = [AbortSignal.timeout(options.timeoutMs), ...(options.signal ? [options.signal] : [])];
|
|
34
|
-
|
|
48
|
+
const fetchJson: FetchJson = options.fetch ?? fetch;
|
|
49
|
+
let response: Awaited<ReturnType<FetchJson>>;
|
|
35
50
|
try {
|
|
36
|
-
response = await
|
|
51
|
+
response = await fetchJson(url, { signal: AbortSignal.any(signals), headers: { accept: "application/json", ...options.headers } });
|
|
37
52
|
} catch (error) {
|
|
38
53
|
const reason = (error as { name?: string }).name === "TimeoutError" ? `no answer in ${options.timeoutMs} ms` : "the request failed";
|
|
39
|
-
throw new Error(`${service} did not answer: ${reason}
|
|
54
|
+
throw new Error(`${service} did not answer: ${reason}`, { cause: error });
|
|
40
55
|
}
|
|
41
56
|
if (!response.ok) throw new Error(`${service} gave HTTP ${response.status}`);
|
|
42
57
|
return response.json();
|
|
@@ -54,13 +69,14 @@ export async function geocode(
|
|
|
54
69
|
baseUrl?: string | undefined;
|
|
55
70
|
timeoutMs: number;
|
|
56
71
|
signal?: AbortSignal | undefined;
|
|
72
|
+
fetch?: FetchJson | undefined;
|
|
57
73
|
}
|
|
58
74
|
): Promise<GeocodeResult | undefined> {
|
|
59
75
|
const params = new URLSearchParams({ format: "jsonv2", limit: "1", q: query });
|
|
60
76
|
if (options.countryCodes !== undefined) params.set("countrycodes", options.countryCodes);
|
|
61
77
|
const rows = (await getJson(
|
|
62
78
|
`${options.baseUrl ?? NOMINATIM_URL}/search?${params.toString()}`,
|
|
63
|
-
{ timeoutMs: options.timeoutMs, signal: options.signal, headers: { "user-agent": options.userAgent } },
|
|
79
|
+
{ timeoutMs: options.timeoutMs, signal: options.signal, headers: { "user-agent": options.userAgent }, fetch: options.fetch },
|
|
64
80
|
"Nominatim"
|
|
65
81
|
)) as Array<{ lat?: string; lon?: string; display_name?: string }>;
|
|
66
82
|
const first = Array.isArray(rows) ? rows[0] : undefined;
|
|
@@ -74,11 +90,11 @@ export async function geocode(
|
|
|
74
90
|
export async function drivingRoute(
|
|
75
91
|
from: Point,
|
|
76
92
|
to: Point,
|
|
77
|
-
options: { baseUrl?: string | undefined; timeoutMs: number; signal?: AbortSignal | undefined }
|
|
93
|
+
options: { baseUrl?: string | undefined; timeoutMs: number; signal?: AbortSignal | undefined; fetch?: FetchJson | undefined }
|
|
78
94
|
): Promise<RouteResult> {
|
|
79
95
|
const body = (await getJson(
|
|
80
96
|
`${options.baseUrl ?? OSRM_URL}/route/v1/driving/${from.lon},${from.lat};${to.lon},${to.lat}?overview=false`,
|
|
81
|
-
{ timeoutMs: options.timeoutMs, signal: options.signal },
|
|
97
|
+
{ timeoutMs: options.timeoutMs, signal: options.signal, fetch: options.fetch },
|
|
82
98
|
"OSRM"
|
|
83
99
|
)) as { code?: string; routes?: Array<{ duration?: number; distance?: number }> };
|
|
84
100
|
const route = body.routes?.[0];
|
package/src/geohash.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The geohash of a point, and the cell of a geohash. A geohash interleaves the bits of the
|
|
3
|
+
* longitude and the latitude, and writes five bits as one character of a base-32 alphabet.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Point } from "./geo.js";
|
|
7
|
+
|
|
8
|
+
/** The base-32 alphabet of the geohash. It has no `a`, `i`, `l` or `o`. */
|
|
9
|
+
export const GEOHASH_ALPHABET = "0123456789bcdefghjkmnpqrstuvwxyz";
|
|
10
|
+
|
|
11
|
+
/** The precision (number of characters) when the caller gives none. At the equator, a cell of 7 characters is approximately 153 m by 153 m. */
|
|
12
|
+
export const DEFAULT_GEOHASH_PRECISION = 7;
|
|
13
|
+
|
|
14
|
+
/** The maximum precision. A cell of 12 characters is smaller than 4 cm by 4 cm. */
|
|
15
|
+
export const MAX_GEOHASH_PRECISION = 12;
|
|
16
|
+
|
|
17
|
+
/** The cell of a geohash: its edges, in degrees, and its center. */
|
|
18
|
+
export interface GeohashCell {
|
|
19
|
+
readonly south: number;
|
|
20
|
+
readonly west: number;
|
|
21
|
+
readonly north: number;
|
|
22
|
+
readonly east: number;
|
|
23
|
+
readonly center: Point;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const checkPoint = (point: Point): void => {
|
|
27
|
+
if (!Number.isFinite(point.lat) || point.lat < -90 || point.lat > 90) throw new RangeError(`The latitude ${point.lat} is not from -90 to 90`);
|
|
28
|
+
if (!Number.isFinite(point.lon) || point.lon < -180 || point.lon > 180) throw new RangeError(`The longitude ${point.lon} is not from -180 to 180`);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* This function gives the geohash of a point. The first bit is the longitude. A coordinate on
|
|
33
|
+
* the middle of a range goes to the upper half.
|
|
34
|
+
*/
|
|
35
|
+
export const geohash = (point: Point, precision: number = DEFAULT_GEOHASH_PRECISION): string => {
|
|
36
|
+
checkPoint(point);
|
|
37
|
+
if (!Number.isInteger(precision) || precision < 1 || precision > MAX_GEOHASH_PRECISION) {
|
|
38
|
+
throw new RangeError(`The precision ${precision} is not an integer from 1 to ${MAX_GEOHASH_PRECISION}`);
|
|
39
|
+
}
|
|
40
|
+
const latRange = [-90, 90] as [number, number];
|
|
41
|
+
const lonRange = [-180, 180] as [number, number];
|
|
42
|
+
let hash = "";
|
|
43
|
+
let bits = 0;
|
|
44
|
+
let value = 0;
|
|
45
|
+
let evenBit = true;
|
|
46
|
+
while (hash.length < precision) {
|
|
47
|
+
const range = evenBit ? lonRange : latRange;
|
|
48
|
+
const coordinate = evenBit ? point.lon : point.lat;
|
|
49
|
+
const mid = (range[0] + range[1]) / 2;
|
|
50
|
+
if (coordinate >= mid) {
|
|
51
|
+
value = (value << 1) | 1;
|
|
52
|
+
range[0] = mid;
|
|
53
|
+
} else {
|
|
54
|
+
value <<= 1;
|
|
55
|
+
range[1] = mid;
|
|
56
|
+
}
|
|
57
|
+
evenBit = !evenBit;
|
|
58
|
+
bits += 1;
|
|
59
|
+
if (bits === 5) {
|
|
60
|
+
hash += GEOHASH_ALPHABET[value];
|
|
61
|
+
bits = 0;
|
|
62
|
+
value = 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return hash;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* This function gives the cell of a geohash: its edges and its center. Upper case letters are
|
|
70
|
+
* correct. It throws a `RangeError` for a character that is not in the alphabet.
|
|
71
|
+
*/
|
|
72
|
+
export const decodeGeohash = (hash: string): GeohashCell => {
|
|
73
|
+
if (hash.length === 0 || hash.length > MAX_GEOHASH_PRECISION) {
|
|
74
|
+
throw new RangeError(`A geohash has 1 to ${MAX_GEOHASH_PRECISION} characters`);
|
|
75
|
+
}
|
|
76
|
+
const lat = [-90, 90] as [number, number];
|
|
77
|
+
const lon = [-180, 180] as [number, number];
|
|
78
|
+
let evenBit = true;
|
|
79
|
+
for (const char of hash.toLowerCase()) {
|
|
80
|
+
const value = GEOHASH_ALPHABET.indexOf(char);
|
|
81
|
+
if (value === -1) throw new RangeError(`"${char}" is not a character of a geohash`);
|
|
82
|
+
for (let bit = 4; bit >= 0; bit -= 1) {
|
|
83
|
+
const range = evenBit ? lon : lat;
|
|
84
|
+
const mid = (range[0] + range[1]) / 2;
|
|
85
|
+
if ((value >> bit) & 1) range[0] = mid;
|
|
86
|
+
else range[1] = mid;
|
|
87
|
+
evenBit = !evenBit;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
south: lat[0],
|
|
92
|
+
west: lon[0],
|
|
93
|
+
north: lat[1],
|
|
94
|
+
east: lon[1],
|
|
95
|
+
center: { lat: (lat[0] + lat[1]) / 2, lon: (lon[0] + lon[1]) / 2 },
|
|
96
|
+
};
|
|
97
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* geo.distance procedure
|
|
3
|
+
*
|
|
4
|
+
* Give the great-circle distance between two points
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { greatCircleKm } from "../../distance.js";
|
|
8
|
+
import type { GeoDistanceInput, GeoDistanceOutput } from "../../types.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* This function gives the great-circle distance between two points, in kilometres. It changes
|
|
12
|
+
* nothing.
|
|
13
|
+
*/
|
|
14
|
+
export async function geoDistance(input: GeoDistanceInput): Promise<GeoDistanceOutput> {
|
|
15
|
+
return { km: greatCircleKm(input.from, input.to) };
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* geo.geohash procedure
|
|
3
|
+
*
|
|
4
|
+
* Give the geohash of a point
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { decodeGeohash, geohash } from "../../geohash.js";
|
|
8
|
+
import type { GeoGeohashInput, GeoGeohashOutput } from "../../types.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* This function gives the geohash of a point and the cell of that geohash. It changes nothing.
|
|
12
|
+
*/
|
|
13
|
+
export async function geoGeohash(input: GeoGeohashInput): Promise<GeoGeohashOutput> {
|
|
14
|
+
const hash = geohash(input.point, input.precision);
|
|
15
|
+
return { hash, cell: decodeGeohash(hash) };
|
|
16
|
+
}
|
package/src/procedures.ts
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The procedures of `@procedures/geo`: `geo.geocode` and `geo.
|
|
2
|
+
* The procedures of `@procedures/geo`: `geo.geocode`, `geo.route`, `geo.distance` and `geo.geohash`.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { createProcedure, zodAdapter, type AnyProcedure, type ProcedureContext } from "@procedures/client";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { drivingRoute, geocode, type GeocodeResult, type RouteResult } from "./geo.js";
|
|
8
|
+
import { geoDistance } from "./procedures/geo/distance.js";
|
|
9
|
+
import { geoGeohash } from "./procedures/geo/geohash.js";
|
|
10
|
+
import {
|
|
11
|
+
GeoDistanceInputSchema,
|
|
12
|
+
GeoDistanceOutputSchema,
|
|
13
|
+
GeoGeohashInputSchema,
|
|
14
|
+
GeoGeohashOutputSchema,
|
|
15
|
+
type GeoDistanceInput,
|
|
16
|
+
type GeoDistanceOutput,
|
|
17
|
+
type GeoGeohashInput,
|
|
18
|
+
type GeoGeohashOutput,
|
|
19
|
+
} from "./types.js";
|
|
8
20
|
|
|
9
21
|
const NETWORK_READ = { reads: ["network"], writes: [] };
|
|
22
|
+
const PURE = { reads: [], writes: [] };
|
|
10
23
|
const point = z.object({ lat: z.number().min(-90).max(90), lon: z.number().min(-180).max(180) });
|
|
11
24
|
const timeoutMs = z.number().int().positive().max(120_000).default(20_000).describe("The time limit, in milliseconds (default: 20000)");
|
|
12
25
|
|
|
@@ -79,4 +92,36 @@ export const geoRouteProcedure: AnyProcedure = createProcedure()
|
|
|
79
92
|
)
|
|
80
93
|
.build() as AnyProcedure;
|
|
81
94
|
|
|
82
|
-
|
|
95
|
+
// =============================================================================
|
|
96
|
+
// geo.distance
|
|
97
|
+
// =============================================================================
|
|
98
|
+
|
|
99
|
+
export const geoDistanceProcedure: AnyProcedure = createProcedure()
|
|
100
|
+
.path(["geo", "distance"])
|
|
101
|
+
.input(zodAdapter<GeoDistanceInput>(GeoDistanceInputSchema))
|
|
102
|
+
.output(zodAdapter<GeoDistanceOutput>(GeoDistanceOutputSchema))
|
|
103
|
+
.meta({
|
|
104
|
+
description: "Give the great-circle distance between two points in kilometres (haversine), without the network",
|
|
105
|
+
output: "json",
|
|
106
|
+
effects: PURE,
|
|
107
|
+
})
|
|
108
|
+
.handler(async (input: GeoDistanceInput): Promise<GeoDistanceOutput> => geoDistance(input))
|
|
109
|
+
.build() as AnyProcedure;
|
|
110
|
+
|
|
111
|
+
// =============================================================================
|
|
112
|
+
// geo.geohash
|
|
113
|
+
// =============================================================================
|
|
114
|
+
|
|
115
|
+
export const geoGeohashProcedure: AnyProcedure = createProcedure()
|
|
116
|
+
.path(["geo", "geohash"])
|
|
117
|
+
.input(zodAdapter<GeoGeohashInput>(GeoGeohashInputSchema))
|
|
118
|
+
.output(zodAdapter<GeoGeohashOutput>(GeoGeohashOutputSchema))
|
|
119
|
+
.meta({
|
|
120
|
+
description: "Give the geohash of a point and the cell of that geohash, without the network",
|
|
121
|
+
output: "json",
|
|
122
|
+
effects: PURE,
|
|
123
|
+
})
|
|
124
|
+
.handler(async (input: GeoGeohashInput): Promise<GeoGeohashOutput> => geoGeohash(input))
|
|
125
|
+
.build() as AnyProcedure;
|
|
126
|
+
|
|
127
|
+
export const geoProcedures: AnyProcedure[] = [geoGeocodeProcedure, geoRouteProcedure, geoDistanceProcedure, geoGeohashProcedure];
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions of the procedures of this package
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import type { Point } from "./geo.js";
|
|
7
|
+
import type { GeohashCell } from "./geohash.js";
|
|
8
|
+
|
|
9
|
+
const point = z.object({ lat: z.number().min(-90).max(90), lon: z.number().min(-180).max(180) });
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// geo.distance Types - Give the great-circle distance between two points
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
/** The input of `geo.distance`: two points. */
|
|
16
|
+
export interface GeoDistanceInput {
|
|
17
|
+
from: Point;
|
|
18
|
+
to: Point;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The output of `geo.distance`: the great-circle distance in kilometres, without rounding. */
|
|
22
|
+
export interface GeoDistanceOutput {
|
|
23
|
+
km: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const GeoDistanceInputSchema: z.ZodType<GeoDistanceInput> = z.object({
|
|
27
|
+
from: point.describe("The first point (degrees)"),
|
|
28
|
+
to: point.describe("The second point (degrees)"),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const GeoDistanceOutputSchema: z.ZodType<GeoDistanceOutput> = z.object({
|
|
32
|
+
km: z.number().describe("The great-circle distance in kilometres (haversine, Earth radius 6371 km), without rounding"),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// geo.geohash Types - Give the geohash of a point
|
|
37
|
+
// =============================================================================
|
|
38
|
+
|
|
39
|
+
/** The input of `geo.geohash`: a point and the number of characters. */
|
|
40
|
+
export interface GeoGeohashInput {
|
|
41
|
+
point: Point;
|
|
42
|
+
precision: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The output of `geo.geohash`: the geohash and its cell. */
|
|
46
|
+
export interface GeoGeohashOutput {
|
|
47
|
+
hash: string;
|
|
48
|
+
cell: GeohashCell;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const GeoGeohashInputSchema: z.ZodType<GeoGeohashInput, { point: Point; precision?: number | undefined }> = z.object({
|
|
52
|
+
point: point.describe("The point (degrees)"),
|
|
53
|
+
precision: z.number().int().min(1).max(12).default(7).describe("The number of characters, from 1 to 12 (default: 7)"),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const GeoGeohashOutputSchema: z.ZodType<GeoGeohashOutput> = z.object({
|
|
57
|
+
hash: z.string().describe("The geohash"),
|
|
58
|
+
cell: z
|
|
59
|
+
.object({
|
|
60
|
+
south: z.number(),
|
|
61
|
+
west: z.number(),
|
|
62
|
+
north: z.number(),
|
|
63
|
+
east: z.number(),
|
|
64
|
+
center: z.object({ lat: z.number(), lon: z.number() }),
|
|
65
|
+
})
|
|
66
|
+
.describe("The cell of the geohash: its edges and its center (degrees)"),
|
|
67
|
+
});
|