@procedures/geo 0.6.0 → 0.8.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 +32 -0
- package/README.md +19 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mercator.d.ts +45 -0
- package/dist/mercator.d.ts.map +1 -0
- package/dist/mercator.js +57 -0
- package/dist/mercator.js.map +1 -0
- package/dist/tiles.d.ts +62 -0
- package/dist/tiles.d.ts.map +1 -0
- package/dist/tiles.js +76 -0
- package/dist/tiles.js.map +1 -0
- package/package.json +6 -2
- package/src/index.ts +1 -0
- package/src/mercator.ts +87 -0
- package/src/tiles.ts +105 -0
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Mercator, the projection of the slippy maps (OpenStreetMap and the others): the pixel of a
|
|
3
|
+
* point at a zoom level, the zoom that fits points into a box, and the 256-pixel tiles that cover a
|
|
4
|
+
* view. The functions are pure and need no network, so a browser bundle can use them.
|
|
5
|
+
*/
|
|
6
|
+
/** The size of a map tile, in pixels. */
|
|
7
|
+
export declare const TILE_SIZE = 256;
|
|
8
|
+
/** A position on the world map at one zoom level, in pixels from the top left corner. */
|
|
9
|
+
export interface Pixel {
|
|
10
|
+
readonly x: number;
|
|
11
|
+
readonly y: number;
|
|
12
|
+
}
|
|
13
|
+
/** This function gives the pixel of a point (degrees) at a zoom level. */
|
|
14
|
+
export declare const project: (lat: number, lon: number, zoom: number) => Pixel;
|
|
15
|
+
/** The options of `fitZoom`. */
|
|
16
|
+
export interface FitZoomOptions {
|
|
17
|
+
/** The deepest zoom to try (default 17). */
|
|
18
|
+
readonly deepest?: number;
|
|
19
|
+
/** The part of the box that the points can use (default 0.85: a margin of 15 %). */
|
|
20
|
+
readonly margin?: number;
|
|
21
|
+
/** The zoom for one point or none, which have no extent (default 14). */
|
|
22
|
+
readonly single?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* This function gives the deepest zoom at which all the points fit in a box of `width` by
|
|
26
|
+
* `height` pixels, with a margin. It tries each zoom from `deepest` down, and gives 1 when none fits.
|
|
27
|
+
*/
|
|
28
|
+
export declare const fitZoom: (points: ReadonlyArray<{
|
|
29
|
+
readonly lat: number;
|
|
30
|
+
readonly lon: number;
|
|
31
|
+
}>, width: number, height: number, options?: FitZoomOptions) => number;
|
|
32
|
+
/** A tile of a view: its tile numbers (`x` wraps around the world) and its place in the view. */
|
|
33
|
+
export interface ViewTile {
|
|
34
|
+
readonly x: number;
|
|
35
|
+
readonly y: number;
|
|
36
|
+
readonly left: number;
|
|
37
|
+
readonly top: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* This function gives the tiles that a view of `width` by `height` pixels needs, when the top left
|
|
41
|
+
* corner of the view is at `topLeft`. Each tile has its tile numbers and its position in the view.
|
|
42
|
+
* The tiles above and below the world are left out.
|
|
43
|
+
*/
|
|
44
|
+
export declare const tilesFor: (topLeft: Pixel, width: number, height: number, zoom: number) => ViewTile[];
|
|
45
|
+
//# sourceMappingURL=mercator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mercator.d.ts","sourceRoot":"","sources":["../src/mercator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,yCAAyC;AACzC,eAAO,MAAM,SAAS,MAAM,CAAC;AAE7B,yFAAyF;AACzF,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,0EAA0E;AAC1E,eAAO,MAAM,OAAO,QAAS,MAAM,OAAO,MAAM,QAAQ,MAAM,KAAG,KAOhE,CAAC;AAEF,gCAAgC;AAChC,MAAM,WAAW,cAAc;IAC7B,4CAA4C;IAC5C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,oFAAoF;IACpF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,yEAAyE;IACzE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;GAGG;AACH,eAAO,MAAM,OAAO,WACV,aAAa,CAAC;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,SAC9D,MAAM,UACL,MAAM,YACL,cAAc,KACtB,MAYF,CAAC;AAEF,iGAAiG;AACjG,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,eAAO,MAAM,QAAQ,YAAa,KAAK,SAAS,MAAM,UAAU,MAAM,QAAQ,MAAM,KAAG,QAAQ,EAe9F,CAAC"}
|
package/dist/mercator.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Mercator, the projection of the slippy maps (OpenStreetMap and the others): the pixel of a
|
|
3
|
+
* point at a zoom level, the zoom that fits points into a box, and the 256-pixel tiles that cover a
|
|
4
|
+
* view. The functions are pure and need no network, so a browser bundle can use them.
|
|
5
|
+
*/
|
|
6
|
+
/** The size of a map tile, in pixels. */
|
|
7
|
+
export const TILE_SIZE = 256;
|
|
8
|
+
/** This function gives the pixel of a point (degrees) at a zoom level. */
|
|
9
|
+
export const project = (lat, lon, zoom) => {
|
|
10
|
+
const scale = TILE_SIZE * 2 ** zoom;
|
|
11
|
+
const sin = Math.sin((lat * Math.PI) / 180);
|
|
12
|
+
return {
|
|
13
|
+
x: ((lon + 180) / 360) * scale,
|
|
14
|
+
y: (0.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI)) * scale,
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* This function gives the deepest zoom at which all the points fit in a box of `width` by
|
|
19
|
+
* `height` pixels, with a margin. It tries each zoom from `deepest` down, and gives 1 when none fits.
|
|
20
|
+
*/
|
|
21
|
+
export const fitZoom = (points, width, height, options = {}) => {
|
|
22
|
+
if (points.length <= 1)
|
|
23
|
+
return options.single ?? 14;
|
|
24
|
+
const margin = options.margin ?? 0.85;
|
|
25
|
+
for (let zoom = options.deepest ?? 17; zoom > 1; zoom -= 1) {
|
|
26
|
+
const pixels = points.map((point) => project(point.lat, point.lon, zoom));
|
|
27
|
+
const xs = pixels.map((pixel) => pixel.x);
|
|
28
|
+
const ys = pixels.map((pixel) => pixel.y);
|
|
29
|
+
if (Math.max(...xs) - Math.min(...xs) <= width * margin && Math.max(...ys) - Math.min(...ys) <= height * margin) {
|
|
30
|
+
return zoom;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return 1;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* This function gives the tiles that a view of `width` by `height` pixels needs, when the top left
|
|
37
|
+
* corner of the view is at `topLeft`. Each tile has its tile numbers and its position in the view.
|
|
38
|
+
* The tiles above and below the world are left out.
|
|
39
|
+
*/
|
|
40
|
+
export const tilesFor = (topLeft, width, height, zoom) => {
|
|
41
|
+
const count = 2 ** zoom;
|
|
42
|
+
const out = [];
|
|
43
|
+
for (let tx = Math.floor(topLeft.x / TILE_SIZE); tx * TILE_SIZE < topLeft.x + width; tx += 1) {
|
|
44
|
+
for (let ty = Math.floor(topLeft.y / TILE_SIZE); ty * TILE_SIZE < topLeft.y + height; ty += 1) {
|
|
45
|
+
if (ty < 0 || ty >= count)
|
|
46
|
+
continue;
|
|
47
|
+
out.push({
|
|
48
|
+
x: ((tx % count) + count) % count,
|
|
49
|
+
y: ty,
|
|
50
|
+
left: tx * TILE_SIZE - topLeft.x,
|
|
51
|
+
top: ty * TILE_SIZE - topLeft.y,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=mercator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mercator.js","sourceRoot":"","sources":["../src/mercator.ts"],"names":[],"mappings":"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"}
|
package/dist/tiles.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map tiles on disk, the Node entry `@procedures/geo/tiles`. The root of the package stays
|
|
3
|
+
* browser-safe, so the functions that read and write files are here.
|
|
4
|
+
*
|
|
5
|
+
* A slippy map asks for many tiles in each view. The policy of the OpenStreetMap tile servers
|
|
6
|
+
* asks each client for a real `User-Agent` and for a cache. `cachedTile` gets a tile from the
|
|
7
|
+
* server one time, keeps it in a folder, and gives the kept file after that. An area that was
|
|
8
|
+
* shown one time works without the network.
|
|
9
|
+
*/
|
|
10
|
+
/** A tile of a slippy map: the zoom level, the column and the row. */
|
|
11
|
+
export interface Tile {
|
|
12
|
+
readonly z: number;
|
|
13
|
+
readonly x: number;
|
|
14
|
+
readonly y: number;
|
|
15
|
+
}
|
|
16
|
+
/** The address of the standard tiles of OpenStreetMap. `{z}`, `{x}` and `{y}` are filled in. */
|
|
17
|
+
export declare const OSM_TILES = "https://tile.openstreetmap.org/{z}/{x}/{y}.png";
|
|
18
|
+
/** The fetch that `cachedTile` uses. A test or a host gives its own with the option `fetch`. */
|
|
19
|
+
export type FetchBytes = (url: string, init: {
|
|
20
|
+
headers: Record<string, string>;
|
|
21
|
+
signal: AbortSignal;
|
|
22
|
+
}) => Promise<{
|
|
23
|
+
ok: boolean;
|
|
24
|
+
status: number;
|
|
25
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
26
|
+
}>;
|
|
27
|
+
/** The options of `cachedTile`. */
|
|
28
|
+
export interface CachedTileOptions {
|
|
29
|
+
/** The folder of the kept tiles. A tile is kept as `<dir>/<z>/<x>/<y>.<extension>`. */
|
|
30
|
+
readonly dir: string;
|
|
31
|
+
/** The `User-Agent` of the requests. The policy of the tile server asks for a real one. */
|
|
32
|
+
readonly userAgent: string;
|
|
33
|
+
/** The address of a tile, with `{z}`, `{x}` and `{y}`. The default is `OSM_TILES`. */
|
|
34
|
+
readonly source?: string | undefined;
|
|
35
|
+
/** The extension of the kept files. The default is `png`. */
|
|
36
|
+
readonly extension?: string | undefined;
|
|
37
|
+
/** The time limit of a request, in milliseconds. The default is 20 000. */
|
|
38
|
+
readonly timeoutMs?: number | undefined;
|
|
39
|
+
/** The largest tile, in bytes. A larger body is not a tile. The default is 2 MiB. */
|
|
40
|
+
readonly maxBytes?: number | undefined;
|
|
41
|
+
/** This option replaces the global `fetch`. */
|
|
42
|
+
readonly fetch?: FetchBytes | undefined;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* This function tells if a tile exists at its zoom level: the three numbers are integers, the zoom
|
|
46
|
+
* is from 0 to `maxZoom`, and the column and the row are from 0 to 2^z - 1.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isTile(tile: Tile, maxZoom?: number): boolean;
|
|
49
|
+
/** This function fills `{z}`, `{x}` and `{y}` of a tile address. */
|
|
50
|
+
export declare function tileUrl(template: string, tile: Tile): string;
|
|
51
|
+
/**
|
|
52
|
+
* This function gives the bytes of a tile. It reads the kept file when there is one. Otherwise it
|
|
53
|
+
* gets the tile from the server, keeps it, and gives it.
|
|
54
|
+
*
|
|
55
|
+
* - A tile that does not exist (`isTile` with zoom 30) throws a `RangeError`.
|
|
56
|
+
* - An HTTP error, no answer in the time limit, or a body larger than `maxBytes` throws an `Error`.
|
|
57
|
+
* Then the function keeps nothing.
|
|
58
|
+
* - The file is written to a temporary name first, then renamed. A stop during the write never
|
|
59
|
+
* leaves a part of a tile.
|
|
60
|
+
*/
|
|
61
|
+
export declare function cachedTile(tile: Tile, options: CachedTileOptions): Promise<Uint8Array>;
|
|
62
|
+
//# sourceMappingURL=tiles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tiles.d.ts","sourceRoot":"","sources":["../src/tiles.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,sEAAsE;AACtE,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,gGAAgG;AAChG,eAAO,MAAM,SAAS,mDAAmD,CAAC;AAE1E,gGAAgG;AAChG,MAAM,MAAM,UAAU,GAAG,CACvB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,MAAM,EAAE,WAAW,CAAA;CAAE,KAC3D,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,IAAI,OAAO,CAAC,WAAW,CAAC,CAAA;CAAE,CAAC,CAAC;AAEnF,mCAAmC;AACnC,MAAM,WAAW,iBAAiB;IAChC,uFAAuF;IACvF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,2FAA2F;IAC3F,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,sFAAsF;IACtF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,6DAA6D;IAC7D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,qFAAqF;IACrF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,+CAA+C;IAC/C,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;CACzC;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,SAAK,GAAG,OAAO,CAMxD;AAED,oEAAoE;AACpE,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,MAAM,CAE5D;AAID;;;;;;;;;GASG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,UAAU,CAAC,CA4B5F"}
|
package/dist/tiles.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map tiles on disk, the Node entry `@procedures/geo/tiles`. The root of the package stays
|
|
3
|
+
* browser-safe, so the functions that read and write files are here.
|
|
4
|
+
*
|
|
5
|
+
* A slippy map asks for many tiles in each view. The policy of the OpenStreetMap tile servers
|
|
6
|
+
* asks each client for a real `User-Agent` and for a cache. `cachedTile` gets a tile from the
|
|
7
|
+
* server one time, keeps it in a folder, and gives the kept file after that. An area that was
|
|
8
|
+
* shown one time works without the network.
|
|
9
|
+
*/
|
|
10
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
/** The address of the standard tiles of OpenStreetMap. `{z}`, `{x}` and `{y}` are filled in. */
|
|
13
|
+
export const OSM_TILES = "https://tile.openstreetmap.org/{z}/{x}/{y}.png";
|
|
14
|
+
/**
|
|
15
|
+
* This function tells if a tile exists at its zoom level: the three numbers are integers, the zoom
|
|
16
|
+
* is from 0 to `maxZoom`, and the column and the row are from 0 to 2^z - 1.
|
|
17
|
+
*/
|
|
18
|
+
export function isTile(tile, maxZoom = 19) {
|
|
19
|
+
const { z, x, y } = tile;
|
|
20
|
+
if (!Number.isSafeInteger(z) || !Number.isSafeInteger(x) || !Number.isSafeInteger(y))
|
|
21
|
+
return false;
|
|
22
|
+
if (z < 0 || z > maxZoom)
|
|
23
|
+
return false;
|
|
24
|
+
const size = 2 ** z;
|
|
25
|
+
return x >= 0 && y >= 0 && x < size && y < size;
|
|
26
|
+
}
|
|
27
|
+
/** This function fills `{z}`, `{x}` and `{y}` of a tile address. */
|
|
28
|
+
export function tileUrl(template, tile) {
|
|
29
|
+
return template.replace("{z}", String(tile.z)).replace("{x}", String(tile.x)).replace("{y}", String(tile.y));
|
|
30
|
+
}
|
|
31
|
+
let written = 0;
|
|
32
|
+
/**
|
|
33
|
+
* This function gives the bytes of a tile. It reads the kept file when there is one. Otherwise it
|
|
34
|
+
* gets the tile from the server, keeps it, and gives it.
|
|
35
|
+
*
|
|
36
|
+
* - A tile that does not exist (`isTile` with zoom 30) throws a `RangeError`.
|
|
37
|
+
* - An HTTP error, no answer in the time limit, or a body larger than `maxBytes` throws an `Error`.
|
|
38
|
+
* Then the function keeps nothing.
|
|
39
|
+
* - The file is written to a temporary name first, then renamed. A stop during the write never
|
|
40
|
+
* leaves a part of a tile.
|
|
41
|
+
*/
|
|
42
|
+
export async function cachedTile(tile, options) {
|
|
43
|
+
if (!isTile(tile, 30))
|
|
44
|
+
throw new RangeError(`cachedTile: ${JSON.stringify(tile)} is not a tile`);
|
|
45
|
+
const file = join(options.dir, String(tile.z), String(tile.x), `${String(tile.y)}.${options.extension ?? "png"}`);
|
|
46
|
+
try {
|
|
47
|
+
return new Uint8Array(await readFile(file));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Not kept yet: get it one time
|
|
51
|
+
}
|
|
52
|
+
const url = tileUrl(options.source ?? OSM_TILES, tile);
|
|
53
|
+
const timeoutMs = options.timeoutMs ?? 20_000;
|
|
54
|
+
const fetchBytes = options.fetch ?? fetch;
|
|
55
|
+
let response;
|
|
56
|
+
try {
|
|
57
|
+
response = await fetchBytes(url, { headers: { "user-agent": options.userAgent }, signal: AbortSignal.timeout(timeoutMs) });
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const reason = error.name === "TimeoutError" ? `no answer in ${String(timeoutMs)} ms` : "the request failed";
|
|
61
|
+
throw new Error(`The tile server did not answer for ${url}: ${reason}`);
|
|
62
|
+
}
|
|
63
|
+
if (!response.ok)
|
|
64
|
+
throw new Error(`The tile server gave HTTP ${String(response.status)} for ${url}`);
|
|
65
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
66
|
+
const maxBytes = options.maxBytes ?? 2 * 1024 * 1024;
|
|
67
|
+
if (bytes.byteLength > maxBytes)
|
|
68
|
+
throw new Error(`The body for ${url} has ${String(bytes.byteLength)} bytes, more than a tile (${String(maxBytes)})`);
|
|
69
|
+
await mkdir(dirname(file), { recursive: true });
|
|
70
|
+
written += 1;
|
|
71
|
+
const temporary = `${file}.${String(process.pid)}-${String(written)}.tmp`;
|
|
72
|
+
await writeFile(temporary, bytes);
|
|
73
|
+
await rename(temporary, file);
|
|
74
|
+
return bytes;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=tiles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tiles.js","sourceRoot":"","sources":["../src/tiles.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@procedures/geo",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.8.0",
|
|
5
5
|
"description": "Map procedures: the coordinates of an address (OpenStreetMap Nominatim), the time and distance of a drive (OSRM), the great-circle distance and the geohash.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"geocode",
|
|
@@ -39,6 +39,10 @@
|
|
|
39
39
|
"./register": {
|
|
40
40
|
"types": "./dist/register.d.ts",
|
|
41
41
|
"import": "./dist/register.js"
|
|
42
|
+
},
|
|
43
|
+
"./tiles": {
|
|
44
|
+
"types": "./dist/tiles.d.ts",
|
|
45
|
+
"import": "./dist/tiles.js"
|
|
42
46
|
}
|
|
43
47
|
},
|
|
44
48
|
"files": [
|
|
@@ -60,7 +64,7 @@
|
|
|
60
64
|
"provenance": true
|
|
61
65
|
},
|
|
62
66
|
"dependencies": {
|
|
63
|
-
"@procedures/client": "0.
|
|
67
|
+
"@procedures/client": "0.8.0",
|
|
64
68
|
"zod": "^4.6.5"
|
|
65
69
|
},
|
|
66
70
|
"devDependencies": {
|
package/src/index.ts
CHANGED
package/src/mercator.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Mercator, the projection of the slippy maps (OpenStreetMap and the others): the pixel of a
|
|
3
|
+
* point at a zoom level, the zoom that fits points into a box, and the 256-pixel tiles that cover a
|
|
4
|
+
* view. The functions are pure and need no network, so a browser bundle can use them.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** The size of a map tile, in pixels. */
|
|
8
|
+
export const TILE_SIZE = 256;
|
|
9
|
+
|
|
10
|
+
/** A position on the world map at one zoom level, in pixels from the top left corner. */
|
|
11
|
+
export interface Pixel {
|
|
12
|
+
readonly x: number;
|
|
13
|
+
readonly y: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** This function gives the pixel of a point (degrees) at a zoom level. */
|
|
17
|
+
export const project = (lat: number, lon: number, zoom: number): Pixel => {
|
|
18
|
+
const scale = TILE_SIZE * 2 ** zoom;
|
|
19
|
+
const sin = Math.sin((lat * Math.PI) / 180);
|
|
20
|
+
return {
|
|
21
|
+
x: ((lon + 180) / 360) * scale,
|
|
22
|
+
y: (0.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI)) * scale,
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** The options of `fitZoom`. */
|
|
27
|
+
export interface FitZoomOptions {
|
|
28
|
+
/** The deepest zoom to try (default 17). */
|
|
29
|
+
readonly deepest?: number;
|
|
30
|
+
/** The part of the box that the points can use (default 0.85: a margin of 15 %). */
|
|
31
|
+
readonly margin?: number;
|
|
32
|
+
/** The zoom for one point or none, which have no extent (default 14). */
|
|
33
|
+
readonly single?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* This function gives the deepest zoom at which all the points fit in a box of `width` by
|
|
38
|
+
* `height` pixels, with a margin. It tries each zoom from `deepest` down, and gives 1 when none fits.
|
|
39
|
+
*/
|
|
40
|
+
export const fitZoom = (
|
|
41
|
+
points: ReadonlyArray<{ readonly lat: number; readonly lon: number }>,
|
|
42
|
+
width: number,
|
|
43
|
+
height: number,
|
|
44
|
+
options: FitZoomOptions = {}
|
|
45
|
+
): number => {
|
|
46
|
+
if (points.length <= 1) return options.single ?? 14;
|
|
47
|
+
const margin = options.margin ?? 0.85;
|
|
48
|
+
for (let zoom = options.deepest ?? 17; zoom > 1; zoom -= 1) {
|
|
49
|
+
const pixels = points.map((point) => project(point.lat, point.lon, zoom));
|
|
50
|
+
const xs = pixels.map((pixel) => pixel.x);
|
|
51
|
+
const ys = pixels.map((pixel) => pixel.y);
|
|
52
|
+
if (Math.max(...xs) - Math.min(...xs) <= width * margin && Math.max(...ys) - Math.min(...ys) <= height * margin) {
|
|
53
|
+
return zoom;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return 1;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** A tile of a view: its tile numbers (`x` wraps around the world) and its place in the view. */
|
|
60
|
+
export interface ViewTile {
|
|
61
|
+
readonly x: number;
|
|
62
|
+
readonly y: number;
|
|
63
|
+
readonly left: number;
|
|
64
|
+
readonly top: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* This function gives the tiles that a view of `width` by `height` pixels needs, when the top left
|
|
69
|
+
* corner of the view is at `topLeft`. Each tile has its tile numbers and its position in the view.
|
|
70
|
+
* The tiles above and below the world are left out.
|
|
71
|
+
*/
|
|
72
|
+
export const tilesFor = (topLeft: Pixel, width: number, height: number, zoom: number): ViewTile[] => {
|
|
73
|
+
const count = 2 ** zoom;
|
|
74
|
+
const out: ViewTile[] = [];
|
|
75
|
+
for (let tx = Math.floor(topLeft.x / TILE_SIZE); tx * TILE_SIZE < topLeft.x + width; tx += 1) {
|
|
76
|
+
for (let ty = Math.floor(topLeft.y / TILE_SIZE); ty * TILE_SIZE < topLeft.y + height; ty += 1) {
|
|
77
|
+
if (ty < 0 || ty >= count) continue;
|
|
78
|
+
out.push({
|
|
79
|
+
x: ((tx % count) + count) % count,
|
|
80
|
+
y: ty,
|
|
81
|
+
left: tx * TILE_SIZE - topLeft.x,
|
|
82
|
+
top: ty * TILE_SIZE - topLeft.y,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
};
|
package/src/tiles.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map tiles on disk, the Node entry `@procedures/geo/tiles`. The root of the package stays
|
|
3
|
+
* browser-safe, so the functions that read and write files are here.
|
|
4
|
+
*
|
|
5
|
+
* A slippy map asks for many tiles in each view. The policy of the OpenStreetMap tile servers
|
|
6
|
+
* asks each client for a real `User-Agent` and for a cache. `cachedTile` gets a tile from the
|
|
7
|
+
* server one time, keeps it in a folder, and gives the kept file after that. An area that was
|
|
8
|
+
* shown one time works without the network.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
|
|
14
|
+
/** A tile of a slippy map: the zoom level, the column and the row. */
|
|
15
|
+
export interface Tile {
|
|
16
|
+
readonly z: number;
|
|
17
|
+
readonly x: number;
|
|
18
|
+
readonly y: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The address of the standard tiles of OpenStreetMap. `{z}`, `{x}` and `{y}` are filled in. */
|
|
22
|
+
export const OSM_TILES = "https://tile.openstreetmap.org/{z}/{x}/{y}.png";
|
|
23
|
+
|
|
24
|
+
/** The fetch that `cachedTile` uses. A test or a host gives its own with the option `fetch`. */
|
|
25
|
+
export type FetchBytes = (
|
|
26
|
+
url: string,
|
|
27
|
+
init: { headers: Record<string, string>; signal: AbortSignal }
|
|
28
|
+
) => Promise<{ ok: boolean; status: number; arrayBuffer(): Promise<ArrayBuffer> }>;
|
|
29
|
+
|
|
30
|
+
/** The options of `cachedTile`. */
|
|
31
|
+
export interface CachedTileOptions {
|
|
32
|
+
/** The folder of the kept tiles. A tile is kept as `<dir>/<z>/<x>/<y>.<extension>`. */
|
|
33
|
+
readonly dir: string;
|
|
34
|
+
/** The `User-Agent` of the requests. The policy of the tile server asks for a real one. */
|
|
35
|
+
readonly userAgent: string;
|
|
36
|
+
/** The address of a tile, with `{z}`, `{x}` and `{y}`. The default is `OSM_TILES`. */
|
|
37
|
+
readonly source?: string | undefined;
|
|
38
|
+
/** The extension of the kept files. The default is `png`. */
|
|
39
|
+
readonly extension?: string | undefined;
|
|
40
|
+
/** The time limit of a request, in milliseconds. The default is 20 000. */
|
|
41
|
+
readonly timeoutMs?: number | undefined;
|
|
42
|
+
/** The largest tile, in bytes. A larger body is not a tile. The default is 2 MiB. */
|
|
43
|
+
readonly maxBytes?: number | undefined;
|
|
44
|
+
/** This option replaces the global `fetch`. */
|
|
45
|
+
readonly fetch?: FetchBytes | undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* This function tells if a tile exists at its zoom level: the three numbers are integers, the zoom
|
|
50
|
+
* is from 0 to `maxZoom`, and the column and the row are from 0 to 2^z - 1.
|
|
51
|
+
*/
|
|
52
|
+
export function isTile(tile: Tile, maxZoom = 19): boolean {
|
|
53
|
+
const { z, x, y } = tile;
|
|
54
|
+
if (!Number.isSafeInteger(z) || !Number.isSafeInteger(x) || !Number.isSafeInteger(y)) return false;
|
|
55
|
+
if (z < 0 || z > maxZoom) return false;
|
|
56
|
+
const size = 2 ** z;
|
|
57
|
+
return x >= 0 && y >= 0 && x < size && y < size;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** This function fills `{z}`, `{x}` and `{y}` of a tile address. */
|
|
61
|
+
export function tileUrl(template: string, tile: Tile): string {
|
|
62
|
+
return template.replace("{z}", String(tile.z)).replace("{x}", String(tile.x)).replace("{y}", String(tile.y));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let written = 0;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* This function gives the bytes of a tile. It reads the kept file when there is one. Otherwise it
|
|
69
|
+
* gets the tile from the server, keeps it, and gives it.
|
|
70
|
+
*
|
|
71
|
+
* - A tile that does not exist (`isTile` with zoom 30) throws a `RangeError`.
|
|
72
|
+
* - An HTTP error, no answer in the time limit, or a body larger than `maxBytes` throws an `Error`.
|
|
73
|
+
* Then the function keeps nothing.
|
|
74
|
+
* - The file is written to a temporary name first, then renamed. A stop during the write never
|
|
75
|
+
* leaves a part of a tile.
|
|
76
|
+
*/
|
|
77
|
+
export async function cachedTile(tile: Tile, options: CachedTileOptions): Promise<Uint8Array> {
|
|
78
|
+
if (!isTile(tile, 30)) throw new RangeError(`cachedTile: ${JSON.stringify(tile)} is not a tile`);
|
|
79
|
+
const file = join(options.dir, String(tile.z), String(tile.x), `${String(tile.y)}.${options.extension ?? "png"}`);
|
|
80
|
+
try {
|
|
81
|
+
return new Uint8Array(await readFile(file));
|
|
82
|
+
} catch {
|
|
83
|
+
// Not kept yet: get it one time
|
|
84
|
+
}
|
|
85
|
+
const url = tileUrl(options.source ?? OSM_TILES, tile);
|
|
86
|
+
const timeoutMs = options.timeoutMs ?? 20_000;
|
|
87
|
+
const fetchBytes: FetchBytes = options.fetch ?? fetch;
|
|
88
|
+
let response: Awaited<ReturnType<FetchBytes>>;
|
|
89
|
+
try {
|
|
90
|
+
response = await fetchBytes(url, { headers: { "user-agent": options.userAgent }, signal: AbortSignal.timeout(timeoutMs) });
|
|
91
|
+
} catch (error) {
|
|
92
|
+
const reason = (error as { name?: string }).name === "TimeoutError" ? `no answer in ${String(timeoutMs)} ms` : "the request failed";
|
|
93
|
+
throw new Error(`The tile server did not answer for ${url}: ${reason}`);
|
|
94
|
+
}
|
|
95
|
+
if (!response.ok) throw new Error(`The tile server gave HTTP ${String(response.status)} for ${url}`);
|
|
96
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
97
|
+
const maxBytes = options.maxBytes ?? 2 * 1024 * 1024;
|
|
98
|
+
if (bytes.byteLength > maxBytes) throw new Error(`The body for ${url} has ${String(bytes.byteLength)} bytes, more than a tile (${String(maxBytes)})`);
|
|
99
|
+
await mkdir(dirname(file), { recursive: true });
|
|
100
|
+
written += 1;
|
|
101
|
+
const temporary = `${file}.${String(process.pid)}-${String(written)}.tmp`;
|
|
102
|
+
await writeFile(temporary, bytes);
|
|
103
|
+
await rename(temporary, file);
|
|
104
|
+
return bytes;
|
|
105
|
+
}
|