Browse Source

Add links to surrounding pages in --map option

Also draw a boundary
mrkvon 8 months ago
parent
commit
acdaa0cbc2
6 changed files with 774 additions and 40 deletions
  1. 3
    1
      package.json
  2. 1
    1
      src/config.ts
  3. 150
    13
      src/map.ts
  4. 40
    7
      src/pdf.ts
  5. 7
    2
      src/route.ts
  6. 573
    16
      yarn.lock

+ 3
- 1
package.json View File

@@ -56,6 +56,7 @@
56 56
     "typescript": "^3.7.5"
57 57
   },
58 58
   "dependencies": {
59
+    "@types/pdfkit": "^0.11.2",
59 60
     "axios": "^0.19.2",
60 61
     "axios-rate-limit": "^1.3.0",
61 62
     "chalk": "^4.1.2",
@@ -63,6 +64,7 @@
63 64
     "gm": "^1.23.1",
64 65
     "haversine": "^1.1.1",
65 66
     "merge-img": "^2.1.3",
66
-    "minimist": "^1.2.0"
67
+    "minimist": "^1.2.0",
68
+    "pdfkit": "^0.12.3"
67 69
   }
68 70
 }

+ 1
- 1
src/config.ts View File

@@ -19,7 +19,7 @@ interface BaseConfig extends ModeConfig {
19 19
   tmp: string;
20 20
 }
21 21
 
22
-interface MapConfig extends BaseConfig {
22
+export interface MapConfig extends BaseConfig {
23 23
   mode: 'map';
24 24
   north: number;
25 25
   west: number;

+ 150
- 13
src/map.ts View File

@@ -1,8 +1,12 @@
1
-import { lat2tile, lon2tile, getTileSize } from './osm';
1
+import { lat2tile, lon2tile, getTileSize, lat2tileExact, lon2tileExact } from './osm';
2 2
 import { TileServer, Page } from './types';
3
-import { createPdf } from './pdf';
3
+import { createPdf, PDFOptions } from './pdf';
4 4
 import { downloadPages } from './download';
5
-import { clearTmp } from './utils';
5
+import { clearTmp, getFilename } from './utils';
6
+import { TILE_SIZE } from './route';
7
+import { MapConfig } from './config';
8
+import gm from 'gm';
9
+import log from './log';
6 10
 
7 11
 // main executable function
8 12
 export default async function map({
@@ -32,13 +36,72 @@ export default async function map({
32 36
   const pages: Page[] = boundaries2pages({ north, west, south, east, pageSizeX, pageSizeY, zoom });
33 37
   // download pages
34 38
   await downloadPages(pages, tmp, tileServer);
39
+  // draw boundary
40
+  await drawBoundary(pages, { north, west, south, east, zoom, tmp, pageSizeX, pageSizeY });
35 41
   // create pdf
36
-  await createPdf(output, tmp);
42
+  await createPdf(output, tmp, {
43
+    pageSizeX,
44
+    pageSizeY,
45
+    links: boundaries2links({ north, west, south, east, pageSizeX, pageSizeY, zoom }),
46
+  });
37 47
   // clean up downloaded pages
38 48
   await clearTmp(tmp);
39 49
 }
40 50
 
41
-function boundaries2pages({
51
+const drawBoundary = async (
52
+  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'>,
63
+): 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
+  for (let i = 0, len = pages.length; i < len; ++i) {
72
+    log(`drawing boundary ${i + 1}/${len}`);
73
+    const page = pages[i];
74
+    const filename = getFilename(tmp, i);
75
+    const control = gm(filename);
76
+
77
+    const pixelBoundary = {
78
+      north: (tileBoundary.north - page.y) * TILE_SIZE,
79
+      south: (tileBoundary.south - page.y) * TILE_SIZE,
80
+      east: (tileBoundary.east - page.x) * TILE_SIZE,
81
+      west: (tileBoundary.west - page.x) * TILE_SIZE,
82
+    };
83
+
84
+    // draw boundary
85
+    const maxWidth = Math.max(pageSizeX, pageSizeY) * TILE_SIZE;
86
+    control.stroke('#000a', maxWidth).fill('#000f');
87
+    control.drawRectangle(
88
+      pixelBoundary.west - maxWidth / 2,
89
+      pixelBoundary.north - maxWidth / 2,
90
+      pixelBoundary.east + maxWidth / 2,
91
+      pixelBoundary.south + maxWidth / 2,
92
+    );
93
+
94
+    await new Promise<void>((resolve, reject) => {
95
+      // draw it
96
+      control.write(filename, err => {
97
+        if (err) return reject(err);
98
+        else return resolve();
99
+      });
100
+    });
101
+  }
102
+};
103
+
104
+const boundaries2links = ({
42 105
   north,
43 106
   west,
44 107
   south,
@@ -54,21 +117,95 @@ function boundaries2pages({
54 117
   pageSizeX: number;
55 118
   pageSizeY: number;
56 119
   zoom: number;
57
-}) {
58
-  const x = lon2tile(west, zoom);
59
-  const y = lat2tile(north, zoom);
120
+}): PDFOptions['links'] => {
121
+  const { width, height } = boundaries2size({ north, west, south, east, pageSizeX, pageSizeY, zoom });
60 122
 
123
+  const links: PDFOptions['links'] = [];
124
+  const pageSize = { width: pageSizeX * TILE_SIZE, height: pageSizeY * TILE_SIZE };
125
+  const breakPoints = {
126
+    x: [0, 0.25 * pageSize.width, 0.75 * pageSize.width, pageSize.width],
127
+    y: [0, 0.25 * pageSize.height, 0.75 * pageSize.height, pageSize.height],
128
+  };
129
+  for (let i = 0, len = width * height; i < len; ++i) {
130
+    const linksForPage: PDFOptions['links'][number] = [];
131
+    for (const yi of [-1, 0, 1]) {
132
+      for (const xi of [-1, 0, 1]) {
133
+        const url = i + yi * width + xi;
134
+        const x = breakPoints.x[xi + 1];
135
+        const y = breakPoints.y[yi + 1];
136
+        const w = breakPoints.x[xi + 2] - breakPoints.x[xi + 1];
137
+        const h = breakPoints.y[yi + 2] - breakPoints.y[yi + 1];
138
+        if (
139
+          url >= 0 &&
140
+          url < len &&
141
+          // we don't want to link to page itself
142
+          url !== i &&
143
+          // we don't want to go beyond left and right edges
144
+          url % width === (i % width) + xi &&
145
+          // we don't want to go beyond top and bottom edges
146
+          Math.floor(url / width) % height === (Math.floor(i / width) % height) + yi
147
+        ) {
148
+          linksForPage.push({ x, y, width: w, height: h, url });
149
+        }
150
+      }
151
+    }
152
+    links.push(linksForPage);
153
+  }
154
+  return links;
155
+};
156
+
157
+const boundaries2size = ({
158
+  north,
159
+  west,
160
+  south,
161
+  east,
162
+  pageSizeX,
163
+  pageSizeY,
164
+  zoom,
165
+}: {
166
+  north: number;
167
+  west: number;
168
+  south: number;
169
+  east: number;
170
+  pageSizeX: number;
171
+  pageSizeY: number;
172
+  zoom: number;
173
+}): { width: number; height: number } => {
61 174
   const { width, height } = getTileSize({ north, west, south, east, zoom });
175
+  return {
176
+    width: Math.ceil(width / pageSizeX),
177
+    height: Math.ceil(height / pageSizeY),
178
+  };
179
+};
62 180
 
63
-  const pagesX = Math.ceil(width / pageSizeX);
64
-  const pagesY = Math.ceil(height / pageSizeY);
181
+function boundaries2pages({
182
+  north,
183
+  west,
184
+  south,
185
+  east,
186
+  pageSizeX,
187
+  pageSizeY,
188
+  zoom,
189
+}: {
190
+  north: number;
191
+  west: number;
192
+  south: number;
193
+  east: number;
194
+  pageSizeX: number;
195
+  pageSizeY: number;
196
+  zoom: number;
197
+}) {
198
+  const { width, height } = boundaries2size({ north, west, south, east, zoom, pageSizeX, pageSizeY });
65 199
 
66
-  console.log('size', pagesX, 'x', pagesY, 'pages'); // tslint:disable-line:no-console
200
+  console.log('size', width, 'x', height, 'pages'); // tslint:disable-line:no-console
67 201
 
68 202
   const pages: Page[] = [];
69 203
 
70
-  for (let py = 0; py < pagesY; py++) {
71
-    for (let px = 0; px < pagesX; px++) {
204
+  const x = lon2tile(west, zoom);
205
+  const y = lat2tile(north, zoom);
206
+
207
+  for (let py = 0; py < height; py++) {
208
+    for (let px = 0; px < width; px++) {
72 209
       pages.push({
73 210
         x: x + px * pageSizeX,
74 211
         y: y + py * pageSizeY,

+ 40
- 7
src/pdf.ts View File

@@ -1,13 +1,46 @@
1
-import gm from 'gm';
1
+import PDFDocument from 'pdfkit';
2 2
 import log from './log';
3
+import fs from 'fs-extra';
4
+import path from 'path';
3 5
 
4
-export async function createPdf(output: string, tmp: string) {
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
+  }[][];
16
+}
17
+
18
+export async function createPdf(output: string, tmp: string, { pageSizeX, pageSizeY, links }: PDFOptions) {
5 19
   log('Creating pdf');
6
-  await new Promise((resolve, reject) => {
7
-    gm(`${tmp}/*.png`).write(`${output}.pdf`, err => {
8
-      if (!err) return resolve();
9
-      if (err) return reject(err);
10
-    });
20
+  const files = await fs.readdir(tmp);
21
+  await new Promise<void>((resolve, reject) => {
22
+    const doc = new PDFDocument({ margin: 0, size: [256 * pageSizeX, 256 * pageSizeY], bufferPages: true });
23
+    const stream = doc.pipe(fs.createWriteStream(`${output}.pdf`));
24
+    let first = true;
25
+    for (const file of files) {
26
+      if (!first) {
27
+        doc.addPage();
28
+      } else {
29
+        first = !first;
30
+      }
31
+      doc.image(path.join(tmp, file));
32
+    }
33
+    for (let i = 0, len = links.length; i < len; ++i) {
34
+      doc.switchToPage(i);
35
+      for (const link of links[i]) {
36
+        const { x, y, width, height, url } = link;
37
+        doc.link(x, y, width, height, url as string);
38
+      }
39
+    }
40
+    doc.end();
41
+
42
+    stream.on('finish', resolve);
43
+    stream.on('error', reject);
11 44
   });
12 45
   // eslint-disable-next-line: no-console
13 46
   log(`Your map was saved to ${output}.pdf\n`);

+ 7
- 2
src/route.ts View File

@@ -10,7 +10,7 @@ import { Tile, Page, TileServer } from './types';
10 10
 import { downloadPages } from './download';
11 11
 import { Coordinate, parseRoute } from './parse-route';
12 12
 
13
-const TILE_SIZE = 256;
13
+export const TILE_SIZE = 256;
14 14
 
15 15
 interface TileWithDistance extends Tile {
16 16
   distance: number;
@@ -74,7 +74,12 @@ export default async function path({
74 74
   }
75 75
 
76 76
   // make pdf from pages
77
-  await createPdf(output, tmp);
77
+  const options = {
78
+    pageSizeX,
79
+    pageSizeY,
80
+    links: [],
81
+  };
82
+  await createPdf(output, tmp, options);
78 83
   await clearTmp(tmp);
79 84
 }
80 85
 

+ 573
- 16
yarn.lock
File diff suppressed because it is too large
View File