Browse Source

Add page of contents with links to map pages

Also lint
mrkvon 8 months ago
parent
commit
4586b18968
10 changed files with 165 additions and 118 deletions
  1. 2
    2
      src/config.ts
  2. 10
    8
      src/download.ts
  3. 110
    76
      src/map.ts
  4. 9
    0
      src/osm.ts
  5. 17
    18
      src/pdf.ts
  6. 8
    10
      src/route.ts
  7. 1
    0
      src/run.ts
  8. 1
    1
      src/tile-servers.ts
  9. 4
    1
      src/types.ts
  10. 3
    2
      src/utils.ts

+ 2
- 2
src/config.ts View File

@@ -1,6 +1,6 @@
1 1
 import minimist from 'minimist';
2 2
 import { TileServer } from './types';
3
-import { servers, parseUrl } from './tile-servers';
3
+import { tileServers, parseUrl } from './tile-servers';
4 4
 // TODO json-schema validation of input
5 5
 
6 6
 // Generic options
@@ -118,7 +118,7 @@ export default function getConfig(): MapConfig | RouteConfig | HelpConfig | Tile
118 118
 
119 119
 function getTileServer(server: string | number, rateLimit: number = 10): TileServer {
120 120
   if (typeof server === 'number') {
121
-    return servers[server - 1];
121
+    return tileServers[server - 1];
122 122
   } else {
123 123
     return {
124 124
       url: parseUrl(server),

+ 10
- 8
src/download.ts View File

@@ -3,19 +3,21 @@ import { getHttp } from './http';
3 3
 import fs from 'fs-extra';
4 4
 import mergeImg from 'merge-img';
5 5
 import log from './log';
6
-import { getFilename, fileExists } from './utils';
6
+import { fileExists } from './utils';
7 7
 
8 8
 // This is the main exported function. Provide pages, folder and server to dowload from and it fills the tmp folder with the pages
9
-export async function downloadPages(pages: Page[], tmp: string, tileServer: TileServer) {
9
+export async function downloadPages(pages: Page[], tmp: string) {
10 10
   await fs.ensureDir(tmp);
11
-  const http = getHttp(tileServer.rateLimit);
11
+  const rateLimit = pages.reduce((_rateLimit, page) => {
12
+    return Math.min(_rateLimit, page.tileServer.rateLimit);
13
+  }, Infinity);
14
+  const http = getHttp(rateLimit);
12 15
   for (let i = 0, len = pages.length; i < len; i++) {
13
-    const filename = getFilename(tmp, i);
14
-    if (!(await fileExists(filename))) {
15
-      const page = pages[i];
16
+    const page = pages[i];
17
+    if (!(await fileExists(page.filename))) {
16 18
       const info = `downloading page ${i + 1}/${pages.length}`;
17
-      const pageTiles = await getPage(page, { http, tileServer }, info);
18
-      await savePage(pageTiles, filename);
19
+      const pageTiles = await getPage(page, { http, tileServer: page.tileServer }, info);
20
+      await savePage(pageTiles, page.filename);
19 21
     }
20 22
   }
21 23
 }

+ 110
- 76
src/map.ts View File

@@ -1,78 +1,102 @@
1
-import { lat2tile, lon2tile, getTileSize, lat2tileExact, lon2tileExact } from './osm';
2
-import { TileServer, Page } from './types';
3
-import { createPdf, PDFOptions } from './pdf';
1
+import { lat2tile, lon2tile, getTileSize, lat2tileExact, lon2tileExact, tile2lat, tile2lon } from './osm';
2
+import { Page, TileServer } from './types';
3
+import { createPdf, PDFLink } from './pdf';
4 4
 import { downloadPages } from './download';
5 5
 import { clearTmp, getFilename } from './utils';
6 6
 import { TILE_SIZE } from './route';
7 7
 import { MapConfig } from './config';
8 8
 import gm from 'gm';
9 9
 import log from './log';
10
+import path from 'path';
10 11
 
11 12
 // main executable function
12
-export default async function map({
13
-  zoom,
14
-  output,
15
-  pageSizeX,
16
-  pageSizeY,
17
-  north,
18
-  west,
19
-  south,
20
-  east,
21
-  tileServer,
22
-  tmp,
23
-}: {
24
-  zoom: number;
25
-  output: string;
26
-  pageSizeX: number;
27
-  pageSizeY: number;
28
-  north: number;
29
-  west: number;
30
-  south: number;
31
-  east: number;
32
-  tileServer: TileServer;
33
-  tmp: string;
34
-}) {
35
-  // collect pages
36
-  const pages: Page[] = boundaries2pages({ north, west, south, east, pageSizeX, pageSizeY, zoom });
13
+export default async function map(config: MapConfig) {
14
+  const { output, tmp } = config;
15
+  // collect content
16
+  const contentPage = getContentPage(config);
17
+
18
+  const pages: Page[] = boundaries2pages(config);
19
+  const links = boundaries2links(config, contentPage ? 1 : 0);
20
+
21
+  if (contentPage) {
22
+    const contentLinks = getContentLinks(contentPage, pages);
23
+    links.unshift(contentLinks);
24
+    pages.unshift(contentPage);
25
+  }
26
+
37 27
   // download pages
38
-  await downloadPages(pages, tmp, tileServer);
28
+  await downloadPages(pages, tmp);
39 29
   // draw boundary
40
-  await drawBoundary(pages, { north, west, south, east, zoom, tmp, pageSizeX, pageSizeY });
30
+  await drawBoundary(pages, config);
41 31
   // create pdf
42
-  await createPdf(output, tmp, {
43
-    pageSizeX,
44
-    pageSizeY,
45
-    links: boundaries2links({ north, west, south, east, pageSizeX, pageSizeY, zoom }),
46
-  });
32
+  await createPdf(output, pages, links);
47 33
   // clean up downloaded pages
48 34
   await clearTmp(tmp);
49 35
 }
50 36
 
37
+const getContentLinks = (contentPage: Page, pages: Page[]): PDFLink[] => {
38
+  const links: PDFLink[] = [];
39
+
40
+  let i = 1;
41
+  pages.forEach(page => {
42
+    const lon = tile2lon(page.x, page.zoom);
43
+    const lat = tile2lat(page.y, page.zoom);
44
+    const lon2 = tile2lon(page.x + page.sx, page.zoom);
45
+    const lat2 = tile2lat(page.y + page.sy, page.zoom);
46
+    links.push({
47
+      x: (lon2tileExact(lon, contentPage.zoom) - contentPage.x) * TILE_SIZE,
48
+      y: (lat2tileExact(lat, contentPage.zoom) - contentPage.y) * TILE_SIZE,
49
+      width: (lon2tileExact(lon2, contentPage.zoom) - lon2tileExact(lon, contentPage.zoom)) * TILE_SIZE,
50
+      height: (lat2tileExact(lat2, contentPage.zoom) - lat2tileExact(lat, contentPage.zoom)) * TILE_SIZE,
51
+      url: i++,
52
+    });
53
+  });
54
+
55
+  return links;
56
+};
57
+
58
+const getContentPage = ({
59
+  north,
60
+  west,
61
+  south,
62
+  east,
63
+  pageSizeX,
64
+  pageSizeY,
65
+  zoom,
66
+  tmp,
67
+  tileServer,
68
+}: MapConfig): Page | undefined => {
69
+  for (; zoom > 0; zoom--) {
70
+    const { width, height } = getTileSize({ north, west, south, east, zoom });
71
+    if (width <= pageSizeX && height <= pageSizeY)
72
+      return {
73
+        x: lon2tile(west, zoom) - Math.floor((pageSizeX - width) / 2),
74
+        y: lat2tile(north, zoom) - Math.floor((pageSizeY - height) / 2),
75
+        zoom,
76
+        sx: pageSizeX,
77
+        sy: pageSizeY,
78
+        tileServer,
79
+        filename: path.join(tmp, 'content.png'),
80
+      };
81
+  }
82
+};
83
+
51 84
 const drawBoundary = async (
52 85
   pages: Page[],
53
-  {
54
-    north,
55
-    west,
56
-    south,
57
-    east,
58
-    zoom,
59
-    tmp,
60
-    pageSizeX,
61
-    pageSizeY,
62
-  }: Pick<MapConfig, 'north' | 'west' | 'south' | 'east' | 'zoom' | 'tmp' | 'pageSizeX' | 'pageSizeY'>,
86
+  { north, west, south, east }: Pick<MapConfig, 'north' | 'west' | 'south' | 'east'>,
63 87
 ): Promise<void> => {
64
-  const tileBoundary = {
65
-    north: lat2tileExact(north, zoom),
66
-    south: lat2tileExact(south, zoom),
67
-    east: lon2tileExact(east, zoom),
68
-    west: lon2tileExact(west, zoom),
69
-  };
70
-
71 88
   for (let i = 0, len = pages.length; i < len; ++i) {
72 89
     log(`drawing boundary ${i + 1}/${len}`);
73 90
     const page = pages[i];
74
-    const filename = getFilename(tmp, i);
75
-    const control = gm(filename);
91
+
92
+    const tileBoundary = {
93
+      north: lat2tileExact(north, page.zoom),
94
+      south: lat2tileExact(south, page.zoom),
95
+      east: lon2tileExact(east, page.zoom),
96
+      west: lon2tileExact(west, page.zoom),
97
+    };
98
+
99
+    const control = gm(page.filename);
76 100
 
77 101
     const pixelBoundary = {
78 102
       north: (tileBoundary.north - page.y) * TILE_SIZE,
@@ -82,7 +106,7 @@ const drawBoundary = async (
82 106
     };
83 107
 
84 108
     // draw boundary
85
-    const maxWidth = Math.max(pageSizeX, pageSizeY) * TILE_SIZE;
109
+    const maxWidth = Math.max(page.sx, page.sy) * TILE_SIZE;
86 110
     control.stroke('#000a', maxWidth).fill('#000f');
87 111
     control.drawRectangle(
88 112
       pixelBoundary.west - maxWidth / 2,
@@ -93,7 +117,7 @@ const drawBoundary = async (
93 117
 
94 118
     await new Promise<void>((resolve, reject) => {
95 119
       // draw it
96
-      control.write(filename, err => {
120
+      control.write(page.filename, err => {
97 121
         if (err) return reject(err);
98 122
         else return resolve();
99 123
       });
@@ -101,33 +125,36 @@ const drawBoundary = async (
101 125
   }
102 126
 };
103 127
 
104
-const boundaries2links = ({
105
-  north,
106
-  west,
107
-  south,
108
-  east,
109
-  pageSizeX,
110
-  pageSizeY,
111
-  zoom,
112
-}: {
113
-  north: number;
114
-  west: number;
115
-  south: number;
116
-  east: number;
117
-  pageSizeX: number;
118
-  pageSizeY: number;
119
-  zoom: number;
120
-}): PDFOptions['links'] => {
128
+const boundaries2links = (
129
+  {
130
+    north,
131
+    west,
132
+    south,
133
+    east,
134
+    pageSizeX,
135
+    pageSizeY,
136
+    zoom,
137
+  }: {
138
+    north: number;
139
+    west: number;
140
+    south: number;
141
+    east: number;
142
+    pageSizeX: number;
143
+    pageSizeY: number;
144
+    zoom: number;
145
+  },
146
+  offset: number,
147
+): PDFLink[][] => {
121 148
   const { width, height } = boundaries2size({ north, west, south, east, pageSizeX, pageSizeY, zoom });
122 149
 
123
-  const links: PDFOptions['links'] = [];
150
+  const links: PDFLink[][] = [];
124 151
   const pageSize = { width: pageSizeX * TILE_SIZE, height: pageSizeY * TILE_SIZE };
125 152
   const breakPoints = {
126 153
     x: [0, 0.25 * pageSize.width, 0.75 * pageSize.width, pageSize.width],
127 154
     y: [0, 0.25 * pageSize.height, 0.75 * pageSize.height, pageSize.height],
128 155
   };
129 156
   for (let i = 0, len = width * height; i < len; ++i) {
130
-    const linksForPage: PDFOptions['links'][number] = [];
157
+    const linksForPage: PDFLink[] = [];
131 158
     for (const yi of [-1, 0, 1]) {
132 159
       for (const xi of [-1, 0, 1]) {
133 160
         const url = i + yi * width + xi;
@@ -145,7 +172,7 @@ const boundaries2links = ({
145 172
           // we don't want to go beyond top and bottom edges
146 173
           Math.floor(url / width) % height === (Math.floor(i / width) % height) + yi
147 174
         ) {
148
-          linksForPage.push({ x, y, width: w, height: h, url });
175
+          linksForPage.push({ x, y, width: w, height: h, url: url + offset });
149 176
         }
150 177
       }
151 178
     }
@@ -154,6 +181,7 @@ const boundaries2links = ({
154 181
   return links;
155 182
 };
156 183
 
184
+// given gps boundaries, return size of the map in pages
157 185
 const boundaries2size = ({
158 186
   north,
159 187
   west,
@@ -186,6 +214,8 @@ function boundaries2pages({
186 214
   pageSizeX,
187 215
   pageSizeY,
188 216
   zoom,
217
+  tileServer,
218
+  tmp,
189 219
 }: {
190 220
   north: number;
191 221
   west: number;
@@ -194,6 +224,8 @@ function boundaries2pages({
194 224
   pageSizeX: number;
195 225
   pageSizeY: number;
196 226
   zoom: number;
227
+  tileServer: TileServer;
228
+  tmp: string;
197 229
 }) {
198 230
   const { width, height } = boundaries2size({ north, west, south, east, zoom, pageSizeX, pageSizeY });
199 231
 
@@ -212,6 +244,8 @@ function boundaries2pages({
212 244
         sx: pageSizeX,
213 245
         sy: pageSizeY,
214 246
         zoom,
247
+        filename: getFilename(tmp, px + py * width),
248
+        tileServer,
215 249
       });
216 250
     }
217 251
   }

+ 9
- 0
src/osm.ts View File

@@ -21,6 +21,15 @@ export function lat2tile(lat: number, zoom: number) {
21 21
   return Math.floor(lat2tileExact(lat, zoom));
22 22
 }
23 23
 
24
+export function tile2lon(x: number, zoom: number): number {
25
+  return (x / Math.pow(2, zoom)) * 360 - 180;
26
+}
27
+
28
+export function tile2lat(y: number, zoom: number): number {
29
+  const n = Math.PI - (2 * Math.PI * y) / Math.pow(2, zoom);
30
+  return (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)));
31
+}
32
+
24 33
 /**
25 34
  * Given map edges, count how many tiles does the map consist of
26 35
  */

+ 17
- 18
src/pdf.ts View File

@@ -1,34 +1,34 @@
1 1
 import PDFDocument from 'pdfkit';
2 2
 import log from './log';
3 3
 import fs from 'fs-extra';
4
-import path from 'path';
4
+import { Page } from './types';
5
+import { TILE_SIZE } from './route';
5 6
 
6
-export interface PDFOptions {
7
-  pageSizeX: number;
8
-  pageSizeY: number;
9
-  links: {
10
-    x: number;
11
-    y: number;
12
-    width: number;
13
-    height: number;
14
-    url: string | number;
15
-  }[][];
7
+export interface PDFLink {
8
+  x: number;
9
+  y: number;
10
+  width: number;
11
+  height: number;
12
+  url: string | number;
16 13
 }
17 14
 
18
-export async function createPdf(output: string, tmp: string, { pageSizeX, pageSizeY, links }: PDFOptions) {
15
+export async function createPdf(output: string, pages: Page[], links: PDFLink[][]) {
19 16
   log('Creating pdf');
20
-  const files = await fs.readdir(tmp);
21 17
   await new Promise<void>((resolve, reject) => {
22
-    const doc = new PDFDocument({ margin: 0, size: [256 * pageSizeX, 256 * pageSizeY], bufferPages: true });
18
+    const doc = new PDFDocument({
19
+      margin: 0,
20
+      size: [TILE_SIZE * pages[0].sx, TILE_SIZE * pages[0].sy],
21
+      bufferPages: true,
22
+    });
23 23
     const stream = doc.pipe(fs.createWriteStream(`${output}.pdf`));
24 24
     let first = true;
25
-    for (const file of files) {
25
+    for (const page of pages) {
26 26
       if (!first) {
27
-        doc.addPage();
27
+        doc.addPage({ margin: 0, size: [TILE_SIZE * page.sx, TILE_SIZE * page.sy] });
28 28
       } else {
29 29
         first = !first;
30 30
       }
31
-      doc.image(path.join(tmp, file));
31
+      doc.image(page.filename);
32 32
     }
33 33
     for (let i = 0, len = links.length; i < len; ++i) {
34 34
       doc.switchToPage(i);
@@ -42,6 +42,5 @@ export async function createPdf(output: string, tmp: string, { pageSizeX, pageSi
42 42
     stream.on('finish', resolve);
43 43
     stream.on('error', reject);
44 44
   });
45
-  // eslint-disable-next-line: no-console
46 45
   log(`Your map was saved to ${output}.pdf\n`);
47 46
 }

+ 8
- 10
src/route.ts View File

@@ -58,10 +58,10 @@ export default async function path({
58 58
   // convert tile points to pixel points of path
59 59
   const drawablePath = getDrawablePath(tileRoute);
60 60
   // collect the pages in abstract form
61
-  const pages = collectPages(tileRoute, pageSizeX, pageSizeY);
61
+  const pages = collectPages(tileRoute, pageSizeX, pageSizeY, tmp, tileServer);
62 62
 
63 63
   // download the tiles and connect them to pages
64
-  await downloadPages(pages, tmp, tileServer);
64
+  await downloadPages(pages, tmp);
65 65
 
66 66
   // draw path on pages
67 67
   if (draw) {
@@ -74,12 +74,7 @@ export default async function path({
74 74
   }
75 75
 
76 76
   // make pdf from pages
77
-  const options = {
78
-    pageSizeX,
79
-    pageSizeY,
80
-    links: [],
81
-  };
82
-  await createPdf(output, tmp, options);
77
+  await createPdf(output, pages, []);
83 78
   await clearTmp(tmp);
84 79
 }
85 80
 
@@ -145,7 +140,7 @@ function getDrawablePath(tilePath: Path): DrawablePath {
145 140
 }
146 141
 
147 142
 async function drawPath(page: Page, drawablePath: DrawablePath, distanceStep: number, filename: string) {
148
-  await new Promise((resolve, reject) => {
143
+  await new Promise<void>((resolve, reject) => {
149 144
     const control = gm(filename);
150 145
 
151 146
     // tslint:disable-next-line:no-shadowed-variable
@@ -186,10 +181,11 @@ async function drawPath(page: Page, drawablePath: DrawablePath, distanceStep: nu
186 181
   });
187 182
 }
188 183
 
189
-function collectPages(tileRoute: Path, sx: number, sy: number): Page[] {
184
+function collectPages(tileRoute: Path, sx: number, sy: number, tmp: string, tileServer: TileServer): Page[] {
190 185
   const pages: Page[] = [];
191 186
   const pageDict: { [id: string]: boolean } = {};
192 187
 
188
+  let i = 0;
193 189
   for (const tile of tileRoute) {
194 190
     const pageX = Math.floor(tile.x / sx) * sx;
195 191
     const pageY = Math.floor(tile.y / sy) * sy;
@@ -203,6 +199,8 @@ function collectPages(tileRoute: Path, sx: number, sy: number): Page[] {
203 199
         sx,
204 200
         sy,
205 201
         zoom: tile.zoom,
202
+        filename: getFilename(tmp, i++),
203
+        tileServer,
206 204
       });
207 205
     }
208 206
   }

+ 1
- 0
src/run.ts View File

@@ -43,6 +43,7 @@ import chalk from 'chalk';
43 43
     // tslint:disable-next-line:no-console
44 44
     console.error(e);
45 45
     if (config && 'tmp' in config) {
46
+      // tslint:disable-next-line:no-console
46 47
       console.log(
47 48
         chalk.red(
48 49
           `\n\n************************\n\nError: ${e.message}\n\nYour download failed. You can resume it by adding "--tmp ${config.tmp}" to your command.\n\n************************\n`,

+ 1
- 1
src/tile-servers.ts View File

@@ -76,7 +76,7 @@ export const rawServers = [
76 76
   },
77 77
 ];
78 78
 
79
-export const servers: TileServer[] = rawServers.map(({ url, rateLimit }) => ({
79
+export const tileServers: TileServer[] = rawServers.map(({ url, rateLimit }) => ({
80 80
   url: parseUrl(url),
81 81
   rateLimit,
82 82
 }));

+ 4
- 1
src/types.ts View File

@@ -16,4 +16,7 @@ export interface PageSize {
16 16
   sy: number;
17 17
 }
18 18
 
19
-export interface Page extends Tile, PageSize {}
19
+export interface Page extends Tile, PageSize {
20
+  filename: string;
21
+  tileServer: TileServer;
22
+}

+ 3
- 2
src/utils.ts View File

@@ -1,4 +1,5 @@
1 1
 import fs from 'fs-extra';
2
+import path from 'path';
2 3
 
3 4
 export function pad(num: number, decimals = 5): string {
4 5
   return ('0'.repeat(decimals) + num).slice(-decimals);
@@ -8,9 +9,9 @@ export async function clearTmp(tmp: string) {
8 9
   await fs.remove(tmp);
9 10
 }
10 11
 
11
-export const getFilename = (tmp: string, i: number) => `${tmp}/${pad(i)}.png`;
12
+export const getFilename = (tmp: string, i: number) => path.join(tmp, `${pad(i)}.png`);
12 13
 
13
-export function random<T>(input: Array<T>): T {
14
+export function random<T>(input: T[]): T {
14 15
   return input[Math.floor(Math.random() * input.length)];
15 16
 }
16 17