Browse Source

Continue refactor

- add tile servers and --list-tile-servers
- update readme
- additional minor updates
mrkvon 8 months ago
parent
commit
21ea64175d
8 changed files with 229 additions and 94 deletions
  1. 3
    8
      README.md
  2. 62
    46
      src/config.ts
  3. 11
    3
      src/download.ts
  4. 60
    25
      src/help.ts
  5. 1
    1
      src/route.ts
  6. 1
    1
      src/run.ts
  7. 87
    10
      src/tile-servers.ts
  8. 4
    0
      src/utils.ts

+ 3
- 8
README.md View File

@@ -2,7 +2,7 @@
2 2
 
3 3
 Generate pdf with [OpenStreetMap](https://openstreetmap.org) tiles.
4 4
 
5
-![example output of osm2pdf with --path parameter](https://git.mrkvon.org/mrkvon/osm2pdf/raw/branch/master/example.png)
5
+![example output of osm2pdf with --route parameter](https://git.mrkvon.org/mrkvon/osm2pdf/raw/branch/master/example.png)
6 6
 
7 7
 ## Video
8 8
 
@@ -24,20 +24,15 @@ sudo npm install -g osm2pdf
24 24
 You need to provide a route in gpx format. You can download it on [graphhopper website](https://graphhopper.com/maps/). Find your route and click _GPX export_ button.
25 25
 
26 26
 ```bash
27
-osm2pdf --route --zoom=10 --input=path/to/route.gpx --output=path/to/output
27
+osm2pdf --route --zoom=10 --input=path/to/route.gpx --output=path/to/output --tile-server=1
28 28
 ```
29 29
 
30
-or if you want to also draw the path on the map
31
-
32
-```bash
33
-osm2pdf --path --zoom=10 --input=path/to/route.gpx --output=path/to/output --distance --distance-step=20
34
-```
35 30
 ### Download a map
36 31
 
37 32
 You need to provide boundaries of the area you want to download.
38 33
 
39 34
 ```bash
40
-osm2pdf --north=70.923 --west=-4.373 --south=55.756 --east=27.872 --zoom=9 --output=path/to/output
35
+osm2pdf -n=70.923 -w=-4.373 -s=55.756 -e=27.872 --zoom=9 --output=path/to/output --tile-server=2
41 36
 ```
42 37
 
43 38
 ## Help

+ 62
- 46
src/config.ts View File

@@ -1,7 +1,7 @@
1 1
 import minimist from 'minimist';
2 2
 import { TileServer } from './types';
3
+import { servers, parseUrl } from './tile-servers';
3 4
 // TODO json-schema validation of input
4
-//
5 5
 
6 6
 // Generic options
7 7
 type Mode = 'help' | 'map' | 'route' | 'tiles';
@@ -56,55 +56,71 @@ export default function getConfig(): MapConfig | RouteConfig | HelpConfig | Tile
56 56
     return tilesConfig;
57 57
   }
58 58
 
59
-  const zoom = argv.zoom ?? 12;
60
-  const pageSizeX = argv.x ?? 4;
61
-  const pageSizeY = argv.y ?? 5;
62
-  const tileServer = getTileServer(argv['tile-server'], argv['rate-limit']);
63
-
64
-  const baseConfig = { zoom, pageSizeX, pageSizeY, tileServer };
65
-
66
-  if (argv.route) {
67
-    const input = argv.input;
68
-    if (!input) return helpConfig;
69
-
70
-    return {
71
-      ...baseConfig,
72
-      mode: 'route',
73
-      input,
74
-      draw: argv.path ?? true,
75
-      distance: !!argv.distance,
76
-      distanceStep: argv['distance-step'] ?? 10,
77
-      output: argv.output || 'route',
78
-    };
79
-  }
80
-
81
-  if (argv.map) {
82
-    const north: number = argv.n;
83
-    const west: number = argv.w;
84
-    const south: number = argv.s;
85
-    const east: number = argv.e;
86
-
87
-    if (typeof north !== 'number' || typeof south !== 'number' || typeof east !== 'number' || typeof west !== 'number')
88
-      return helpConfig;
89
-
90
-    const output = argv.output ?? `map_${north}_${west}_${south}_${east}_${zoom}`;
91
-    return {
92
-      ...baseConfig,
93
-      mode: 'map',
94
-      north,
95
-      south,
96
-      west,
97
-      east,
98
-      output,
99
-    };
59
+  if (argv.route || argv.map) {
60
+    const zoom = argv.zoom ?? 12;
61
+    const pageSizeX = argv.x ?? 4;
62
+    const pageSizeY = argv.y ?? 5;
63
+    const tileServer = getTileServer(argv['tile-server'], argv['rate-limit']);
64
+
65
+    const baseConfig = { zoom, pageSizeX, pageSizeY, tileServer };
66
+
67
+    if (argv.route) {
68
+      const input = argv.input;
69
+      if (!input) return helpConfig;
70
+
71
+      return {
72
+        ...baseConfig,
73
+        mode: 'route',
74
+        input,
75
+        draw: !!(argv.path ?? true),
76
+        distance: !!(argv.distance ?? true),
77
+        distanceStep: argv['distance-step'] ?? 10,
78
+        output:
79
+          argv.output ||
80
+          `route-${input
81
+            .split('.')
82
+            .slice(0, -1)
83
+            .join('.')}-${zoom}`,
84
+      };
85
+    }
86
+
87
+    if (argv.map) {
88
+      const north: number = argv.n;
89
+      const west: number = argv.w;
90
+      const south: number = argv.s;
91
+      const east: number = argv.e;
92
+
93
+      if (
94
+        typeof north !== 'number' ||
95
+        typeof south !== 'number' ||
96
+        typeof east !== 'number' ||
97
+        typeof west !== 'number'
98
+      )
99
+        return helpConfig;
100
+
101
+      const output = argv.output ?? `map_${north}_${west}_${south}_${east}_${zoom}`;
102
+      return {
103
+        ...baseConfig,
104
+        mode: 'map',
105
+        north,
106
+        south,
107
+        west,
108
+        east,
109
+        output,
110
+      };
111
+    }
100 112
   }
101 113
 
102 114
   return helpConfig;
103 115
 }
104 116
 
105 117
 function getTileServer(server: string | number, rateLimit: number = 10): TileServer {
106
-  return {
107
-    url: ({ x, y, z }) => `${server}/${z}/${x}/${y}.png`,
108
-    rateLimit: rateLimit ?? 5,
109
-  };
118
+  if (typeof server === 'number') {
119
+    return servers[server - 1];
120
+  } else {
121
+    return {
122
+      url: parseUrl(server),
123
+      rateLimit,
124
+    };
125
+  }
110 126
 }

+ 11
- 3
src/download.ts View File

@@ -11,8 +11,8 @@ export async function downloadPages(pages: Page[], tmp: string, tileServer: Tile
11 11
   const http = getHttp(tileServer.rateLimit);
12 12
   for (let i = 0, len = pages.length; i < len; i++) {
13 13
     const page = pages[i];
14
-    log('page', `${i + 1}/${pages.length}`);
15
-    const pageTiles = await getPage(page, { http, tileServer });
14
+    const info = `downloading page ${i + 1}/${pages.length}`;
15
+    const pageTiles = await getPage(page, { http, tileServer }, info);
16 16
     await savePage(pageTiles, getFilename(tmp, i));
17 17
   }
18 18
 }
@@ -38,12 +38,20 @@ async function getTile(
38 38
 async function getPage(
39 39
   { x, y, sx, sy, zoom }: Page,
40 40
   { http, tileServer }: { http: any; tileServer: TileServer },
41
+  info: string,
41 42
 ): Promise<Buffer[][]> {
43
+  let progress = 0;
44
+  log(info, `[${'.'.repeat(progress)}${' '.repeat(sx * sy - progress)}]`);
42 45
   const rows: Promise<Buffer[]>[] = [];
43 46
   for (let i = 0; i < sx; i++) {
44 47
     const row: Promise<Buffer>[] = [];
45 48
     for (let j = 0; j < sy; j++) {
46
-      row.push(getTile({ x: x + i, y: y + j, zoom }, { http, tileServer }));
49
+      row.push(
50
+        getTile({ x: x + i, y: y + j, zoom }, { http, tileServer }).then(tile => {
51
+          log(info, `[${'.'.repeat(++progress)}${' '.repeat(sx * sy - progress)}]`);
52
+          return tile;
53
+        }),
54
+      );
47 55
     }
48 56
     rows.push(Promise.all(row));
49 57
   }

+ 60
- 25
src/help.ts View File

@@ -4,38 +4,73 @@ Export OpenStreetMap to pdf
4 4
 Usage:
5 5
 osm2pdf [options]
6 6
 
7
+=== Generic options (for Route and Area methods) ===
8
+
9
+--zoom          map zoom (default 12)
10
+--tile-server   url or number of the tile server (see also \`osm2pdf --list-tile-servers\`)
11
+                  find more tile servers at https://wiki.openstreetmap.org/wiki/Tile_servers
12
+                  please respect tile usage policies. download responsibly.
13
+--rate-limit    how many tiles per second can be downloaded (default 10)
14
+--output        name of the generated pdf file (".pdf" will be attached, existing files will be overwritten)
15
+-x              tiles per page horizontally (default 4)
16
+-y              tiles per page vertically (default 5)
17
+
18
+
19
+=== Methods ===
20
+
21
+=== Route ===
22
+
23
+Download route, given route coordinates in GPX format
24
+
7 25
 Options:
8
--h, --help                print this page
9
---route                   (optional) download the route
10
-                          you can download the GPX route file from https://graphhopper.com/maps/
11
-                          find the desired route and click "GPX export" (gpx button)
12
---path                    (optional) download the route and draw the path on it
13
-                          similar to --route option
14
---distance                (optional with --path option) write labels with distance in kilometres to path points
15
---distance-step           (optional with --distance option) distance between distance labels (kilometres); defaults to 10
16
---input <path/to/gpx>     (with --route or --path option) path to GPX route file
17
--n <latitude>   north
18
--w <longitude>  west
19
--s <latitude>   south
20
--e <longitude>  east      latitude or longitude of the desired map boundary (only when --route is not specified)
21
-                          downloads a map within a defined square
22
---zoom <level>            (optional) map zoom (number); defaults to 12; must be < 17
23
---sx <integer>            (optional) amount of tiles per page horizontally; defaults to 4
24
---sy <integer>            (optional) amount of tiles per page vertically; defaults to 5
25
---output <path/to/output> (optional) the desired name of the exported pdf file
26
+--route         execute Route method (required)
27
+--input         path to GPX route file (required)
28
+                  you can download the GPX route file e.g. from https://graphhopper.com/maps/
29
+                  find the desired route and click "GPX export" (gpx button)
30
+--no-path       don't draw path (optional)
31
+--no-distance   don't write labels with kilometers at the path (optional)
32
+--distance-step distance between distance labels (default 10 [kilometers])
26 33
 
27 34
 Examples:
28
-1. Provide map boundaries
35
+osm2pdf --route --input=path/to/some-route.gpx --output some-route-12 --zoom=12 --tile-server=2
36
+osm2pdf --route --input=path/to/other-route.gpx --output other-route-13 --zoom=13 --distance-step=5 --tile-server="http://{a|b}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png" --rate-limit=2
37
+
38
+
39
+=== Area ===
40
+
41
+Download rectangular area, given GPS boundaries
42
+
43
+Options:
44
+--map           execute Area method (required)
45
+-n              latitude of the north boundary (required)
46
+-s              latitude of the south boundary (required)
47
+-e              longitude of the east boundary (required)
48
+-w              longitude of the west boundary (required)
49
+
50
+Example:
51
+osm2pdf --map -n=50.1 -s=50 -w=14.9 -e=15 --output=some-name-15 --tile-server=3 --zoom=15
52
+
53
+
54
+=== List Tile Servers ===
55
+
56
+Print a list of some recommended tile servers to choose from
57
+
58
+Options:
59
+--list-tile-servers   (required)
60
+
61
+Example:
62
+osm2pdf --list-tile-servers
29 63
 
30
-  osm2pdf --zoom=10 -n=15.1 -s=14.9 -e=13.9 -w=13.7
31 64
 
32
-2. Provide a route in GPX format (can be exported at https://graphhopper.com/maps/)
33
-  
34
-  osm2pdf --path --zoom=15 --input=path/to/some_route.gpx --output=my-route --distance --distance-step=5
65
+=== Help ===
35 66
 
36
-  OR (route without highlighted path)
67
+Print help
68
+
69
+Options:
70
+-h, --help                print this page (required)
37 71
 
38
-  osm2pdf --route --zoom=15 --input=path/to/some_route.gpx --output=my-route
72
+Example:
73
+osm2pdf --help
39 74
 `;
40 75
 
41 76
 export default function help(): void {

+ 1
- 1
src/route.ts View File

@@ -66,7 +66,7 @@ export default async function path({
66 66
   if (draw) {
67 67
     for (let i = 0, len = pages.length; i < len; ++i) {
68 68
       // draw paths
69
-      log(`${i + 1}/${pages.length} drawing`);
69
+      log(`drawing page ${i + 1}/${pages.length}`);
70 70
       const page = pages[i];
71 71
       await drawPath(page, drawablePath, distance ? distanceStep : -1, getFilename(tmp, i));
72 72
     }

+ 1
- 1
src/run.ts View File

@@ -12,7 +12,7 @@ import getConfig from './config';
12 12
 import map from './map';
13 13
 import route from './route';
14 14
 import help from './help';
15
-import listTileServers from './tile-servers';
15
+import { listTileServers } from './tile-servers';
16 16
 
17 17
 (async () => {
18 18
   try {

+ 87
- 10
src/tile-servers.ts View File

@@ -1,19 +1,96 @@
1 1
 import { TileServer } from './types';
2
+import { random } from './utils';
3
+import log from './log';
2 4
 
3
-// const tileServer = 'http://a.tile.stamen.com/toner';
4
-// const tileServer = 'https://a.tile.openstreetmap.de'
5
-// const tileServer = 'https://tile.openstreetmap.org';
6
-// const tileServer = 'https://tiles.wmflabs.org/bw-mapnik/'
7
-// const tileServer = 'https://c.tile.openstreetmap.fr/osmfr'
8
-//
5
+export const parseUrl = (a: string) => {
6
+  const r = /^(?<base>.+?)\$?{(?<v1>[xyz]{1})}(?<s1>.+?)\$?{(?<v2>[xyz]{1})}(?<s2>.+?)\$?{(?<v3>[xyz]{1})}(?<end>.*)$/gm;
7
+  const {
8
+    groups: { base, v1, s1, v2, s2, v3, end },
9
+  } = r.exec(a) as { groups: { [name: string]: string } };
10
+  const r2 = /(?<base1>.*)({(?<servers>[a-z0-9|]+)})(?<base2>.*)/gm;
11
+  const {
12
+    groups: { base1, servers, base2 },
13
+  } = (r2.exec(base) ?? { groups: { base1: base, servers: '', base2: '' } }) as { groups: { [name: string]: string } };
9 14
 
10
-export const servers: TileServer[] = [
15
+  return ({ x, y, z }: { x: number; y: number; z: number }) =>
16
+    `${base1}${random(servers.split('|'))}${base2}${v1 === 'x' ? x : v1 === 'y' ? y : z}${s1}${
17
+      v2 === 'x' ? x : v2 === 'y' ? y : z
18
+    }${s2}${v3 === 'x' ? x : v3 === 'y' ? y : z}${end}`;
19
+};
20
+
21
+export const rawServers = [
22
+  {
23
+    name: 'German fork of the Standard tile layer: openstreetmap.de',
24
+    url: 'https://{a|b|c}.tile.openstreetmap.de/{z}/{x}/{y}.png',
25
+    rateLimit: 5,
26
+  },
27
+  {
28
+    name: 'OSM France',
29
+    url: 'https://{a|b|c}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png',
30
+    rateLimit: 1,
31
+  },
32
+  {
33
+    name: 'Humanitarian map style',
34
+    url: 'http://{a|b}.tile.openstreetmap.fr/hot/${z}/${x}/${y}.png',
35
+    rateLimit: 1,
36
+  },
37
+  {
38
+    name: 'OpenTopoMap',
39
+    url: 'https://{a|b|c}.tile.opentopomap.org/{z}/{x}/{y}.png',
40
+    rateLimit: 1,
41
+  },
42
+  {
43
+    name: 'Stamen Toner Black & White map',
44
+    url: 'http://a.tile.stamen.com/toner/{z}/{x}/{y}.png',
45
+    rateLimit: 5,
46
+  },
11 47
   {
12
-    url: ({ z, x, y }) => `https://a.tile.opentopomap.org/${z}/${x}/${y}.png`,
48
+    name: 'mapnik map grayscale',
49
+    url: 'https://tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png',
50
+    rateLimit: 5,
51
+  },
52
+  {
53
+    name: 'mapnik map without labels',
54
+    url: 'https://tiles.wmflabs.org/osm-no-labels/${z}/${x}/${y}.png',
55
+    rateLimit: 5,
56
+  },
57
+  {
58
+    name: 'wmflabs Hike & Bike - Hiking map',
59
+    url: 'https://tiles.wmflabs.org/hikebike/${z}/${x}/${y}.png',
60
+    rateLimit: 1,
61
+  },
62
+  {
63
+    name: 'Mapy.cz Base',
64
+    url: 'https://m{1|2|3|4}.mapserver.mapy.cz/base-m/{z}-{x}-{y}',
65
+    rateLimit: 10,
66
+  },
67
+  {
68
+    name: 'Mapy.cz Turistic',
69
+    url: 'https://m{1|2|3|4}.mapserver.mapy.cz/turist-m/{z}-{x}-{y}',
70
+    rateLimit: 10,
71
+  },
72
+  {
73
+    name: 'CyclOSM: OpenStreetMap-based bicycle map',
74
+    url: 'https://{a|b|c}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png',
13 75
     rateLimit: 1,
14 76
   },
15 77
 ];
16 78
 
17
-export default function listTileServers() {
18
-  return 'tile servers listing TODO';
79
+export const servers: TileServer[] = rawServers.map(({ url, rateLimit }) => ({
80
+  url: parseUrl(url),
81
+  rateLimit,
82
+}));
83
+
84
+export function listTileServers() {
85
+  const output: string[] = [];
86
+
87
+  output.push('');
88
+  rawServers.forEach(({ name, url }, index) => {
89
+    output.push(`${index + 1}. ${name}`);
90
+    output.push(url);
91
+    output.push('');
92
+  });
93
+  output.push('');
94
+
95
+  return log(output.join('\n'));
19 96
 }

+ 4
- 0
src/utils.ts View File

@@ -9,3 +9,7 @@ export async function clearTmp(tmp: string) {
9 9
 }
10 10
 
11 11
 export const getFilename = (tmp: string, i: number) => `${tmp}/${pad(i)}.png`;
12
+
13
+export function random<T>(input: Array<T>): T {
14
+  return input[Math.floor(Math.random() * input.length)];
15
+}