tileserver-gl-light 4.3.4 โ†’ 4.4.1

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/.eslintignore ADDED
@@ -0,0 +1 @@
1
+ public
@@ -1,9 +1,7 @@
1
1
  name: 'Auto Merge PRs'
2
2
 
3
3
  on:
4
- pull_request:
5
- branches:
6
- - master
4
+ workflow_call:
7
5
 
8
6
  permissions:
9
7
  pull-requests: write
@@ -12,8 +10,6 @@ permissions:
12
10
  jobs:
13
11
  automerge:
14
12
  runs-on: ubuntu-latest
15
- if: >
16
- github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]'
17
13
  steps:
18
14
  - uses: fastify/github-action-merge-dependabot@v3
19
15
  with:
@@ -0,0 +1,51 @@
1
+ name: 'Continuous Integration'
2
+
3
+ on:
4
+ workflow_call:
5
+
6
+ permissions:
7
+ checks: write
8
+ contents: read
9
+
10
+ jobs:
11
+ ci:
12
+ runs-on: ubuntu-20.04
13
+ steps:
14
+ - name: Check out repository โœจ (non-dependabot)
15
+ if: ${{ github.actor != 'dependabot[bot]' }}
16
+ uses: actions/checkout@v3
17
+
18
+ - name: Check out repository ๐ŸŽ‰ (dependabot)
19
+ if: ${{ github.actor == 'dependabot[bot]' }}
20
+ uses: actions/checkout@v3
21
+ with:
22
+ ref: ${{ github.event.pull_request.head.sha }}
23
+
24
+ - name: Setup node env ๐Ÿ“ฆ
25
+ uses: actions/setup-node@v3
26
+ with:
27
+ node-version-file: 'package.json'
28
+ check-latest: true
29
+ cache: 'npm'
30
+
31
+ - name: Install dependencies ๐Ÿš€
32
+ run: npm ci --prefer-offline --no-audit --omit=optional
33
+
34
+ - name: Run linter(s) ๐Ÿ’…
35
+ uses: wearerequired/lint-action@v2
36
+ with:
37
+ github_token: ${{ secrets.GITHUB_TOKEN }}
38
+ continue_on_error: false
39
+ git_name: github-actions[bot]
40
+ git_email: github-actions[bot]@users.noreply.github.com
41
+ auto_fix: false
42
+ eslint: true
43
+ eslint_extensions: js,cjs,mjs,ts
44
+ prettier: true
45
+ prettier_extensions: js,cjs,ts,json
46
+
47
+ - name: Run hadolint ๐Ÿณ
48
+ uses: hadolint/hadolint-action@v3.0.0
49
+ with:
50
+ dockerfile: Dockerfile
51
+ ignore: DL3008,DL3015
@@ -1,3 +1,5 @@
1
+ name: 'CodeQL'
2
+
1
3
  on:
2
4
  push:
3
5
  branches:
@@ -1,12 +1,7 @@
1
1
  name: 'Continuous Testing'
2
2
 
3
3
  on:
4
- push:
5
- branches:
6
- - master
7
- pull_request:
8
- branches:
9
- - master
4
+ workflow_call:
10
5
 
11
6
  permissions:
12
7
  checks: write
@@ -55,3 +50,21 @@ jobs:
55
50
 
56
51
  - name: Run tests ๐Ÿงช
57
52
  run: xvfb-run --server-args="-screen 0 1024x768x24" npm test
53
+
54
+ - name: Set up QEMU
55
+ uses: docker/setup-qemu-action@v2
56
+ with:
57
+ platforms: 'arm64'
58
+
59
+ - name: Set up Docker Buildx
60
+ uses: docker/setup-buildx-action@v2
61
+
62
+ - name: Test Docker Build
63
+ uses: docker/build-push-action@v3
64
+ with:
65
+ context: .
66
+ push: false
67
+ platforms: linux/arm64,linux/amd64
68
+ # experimental: https://github.com/docker/build-push-action/blob/master/docs/advanced/cache.md#cache-backend-api
69
+ cache-from: type=gha
70
+ cache-to: type=gha,mode=max
@@ -0,0 +1,43 @@
1
+ name: 'The Pipeline'
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ pull_request:
8
+ branches:
9
+ - master
10
+
11
+ concurrency:
12
+ group: ci-${{ github.ref }}-1
13
+ cancel-in-progress: true
14
+
15
+ jobs:
16
+ extract-branch:
17
+ name: 'Fetch branch'
18
+ runs-on: ubuntu-latest
19
+ outputs:
20
+ current_branch: ${{ steps.get-branch.outputs.current_branch }}
21
+ steps:
22
+ - name: Extract branch name ๐Ÿ•Š
23
+ id: get-branch
24
+ run: echo "current_branch=${GITHUB_REF##*/}" >> $GITHUB_OUTPUT
25
+ ci:
26
+ name: 'CI'
27
+ needs:
28
+ - extract-branch
29
+ uses: maptiler/tileserver-gl/.github/workflows/ci.yml@master
30
+ ct:
31
+ name: 'CT'
32
+ needs:
33
+ - extract-branch
34
+ uses: maptiler/tileserver-gl/.github/workflows/ct.yml@master
35
+ automerger:
36
+ name: 'Automerge Dependabot PRs'
37
+ needs:
38
+ - ci
39
+ - ct
40
+ - extract-branch
41
+ if: >
42
+ github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]'
43
+ uses: maptiler/tileserver-gl/.github/workflows/automerger.yml@master
@@ -1,13 +1,13 @@
1
- name: "Build, Test, Release"
1
+ name: 'Build, Test, Release'
2
2
 
3
- on:
3
+ on:
4
4
  workflow_dispatch:
5
5
  inputs:
6
6
  docker_user:
7
- description: 'Docker Username'
7
+ description: 'Docker Username'
8
8
  required: true
9
9
  docker_token:
10
- description: 'Docker Token'
10
+ description: 'Docker Token'
11
11
  required: true
12
12
  npm_token:
13
13
  description: 'NPM Token'
@@ -15,7 +15,7 @@ on:
15
15
 
16
16
  jobs:
17
17
  release:
18
- name: "Build, Test, Publish"
18
+ name: 'Build, Test, Publish'
19
19
  runs-on: ubuntu-20.04
20
20
  steps:
21
21
  - name: Check out repository โœจ
@@ -86,6 +86,9 @@ jobs:
86
86
  push: true
87
87
  tags: maptiler/tileserver-gl:latest, maptiler/tileserver-gl:v${{ env.PACKAGE_VERSION }}
88
88
  platforms: linux/arm64,linux/amd64
89
+ # experimental: https://github.com/docker/build-push-action/blob/master/docs/advanced/cache.md#cache-backend-api
90
+ cache-from: type=gha
91
+ cache-to: type=gha,mode=max
89
92
 
90
93
  - name: Create Tileserver Light Directory
91
94
  run: node publish.js --no-publish
@@ -110,3 +113,6 @@ jobs:
110
113
  push: true
111
114
  tags: maptiler/tileserver-gl-light:latest, maptiler/tileserver-gl-light:v${{ env.PACKAGE_VERSION }}
112
115
  platforms: linux/arm64,linux/amd64
116
+ # experimental: https://github.com/docker/build-push-action/blob/master/docs/advanced/cache.md#cache-backend-api
117
+ cache-from: type=gha
118
+ cache-to: type=gha,mode=max
package/.hadolint.yml ADDED
@@ -0,0 +1,3 @@
1
+ ignored:
2
+ - DL3008
3
+ - DL3015
package/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ 18
@@ -0,0 +1 @@
1
+ public
package/Dockerfile CHANGED
@@ -13,7 +13,7 @@ RUN set -ex; \
13
13
  apt-get -y --no-install-recommends install \
14
14
  ca-certificates \
15
15
  wget; \
16
- wget -qO- https://deb.nodesource.com/setup_16.x | bash; \
16
+ wget -qO- https://deb.nodesource.com/setup_18.x | bash; \
17
17
  apt-get install -y nodejs; \
18
18
  apt-get -y remove wget; \
19
19
  apt-get -y --purge autoremove; \
package/Dockerfile_test CHANGED
@@ -27,7 +27,7 @@ RUN set -ex; \
27
27
  librsvg2-dev \
28
28
  libcurl4-openssl-dev \
29
29
  libpixman-1-dev; \
30
- wget -qO- https://deb.nodesource.com/setup_16.x | bash; \
30
+ wget -qO- https://deb.nodesource.com/setup_18.x | bash; \
31
31
  apt-get install -y nodejs; \
32
32
  apt-get clean;
33
33
 
@@ -39,6 +39,8 @@ Static images
39
39
 
40
40
  * e.g. ``5.9,45.8|5.9,47.8|10.5,47.8|10.5,45.8|5.9,45.8``
41
41
  * can be provided multiple times
42
+ * or pass the path as per [Maptiler Cloud API](https://docs.maptiler.com/cloud/api/static-maps/)
43
+ * Match pattern: ((fill|stroke|width)\:[^\|]+\|)*((enc:.+)|((-?\d+\.?\d*,-?\d+\.?\d*\|)+(-?\d+\.?\d*,-?\d+\.?\d*)))
42
44
 
43
45
  * ``latlng`` - indicates coordinates are in ``lat,lng`` order rather than the usual ``lng,lat``
44
46
  * ``fill`` - color to use as the fill (e.g. ``red``, ``rgba(255,255,255,0.5)``, ``#0000ff``)
@@ -60,7 +60,7 @@ Alternatively, you can use ``tileserver-gl-light`` package instead, which is pur
60
60
  From source
61
61
  ===========
62
62
 
63
- Make sure you have Node v10 (nvm install 10) and run::
63
+ Make sure you have Node v18 (nvm install 18) and run::
64
64
 
65
65
  npm install
66
66
  node .
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tileserver-gl-light",
3
- "version": "4.3.4",
3
+ "version": "4.4.1",
4
4
  "description": "Map tile server for JSON GL styles - serving vector tiles",
5
5
  "main": "src/main.js",
6
6
  "bin": "src/main.js",
@@ -19,6 +19,7 @@
19
19
  "dependencies": {
20
20
  "@mapbox/glyph-pbf-composite": "0.0.3",
21
21
  "@mapbox/mbtiles": "0.12.1",
22
+ "@mapbox/polyline": "^1.1.1",
22
23
  "@mapbox/sphericalmercator": "1.2.0",
23
24
  "@mapbox/vector-tile": "1.3.1",
24
25
  "@maplibre/maplibre-gl-style-spec": "17.0.2",
@@ -26,17 +27,17 @@
26
27
  "chokidar": "3.5.3",
27
28
  "clone": "2.1.2",
28
29
  "color": "4.2.3",
29
- "commander": "9.4.1",
30
+ "commander": "10.0.0",
30
31
  "cors": "2.8.5",
31
32
  "express": "4.18.2",
32
33
  "handlebars": "4.7.7",
33
34
  "http-shutdown": "1.2.2",
34
35
  "morgan": "1.10.0",
35
36
  "pbf": "3.2.1",
36
- "proj4": "2.8.0",
37
+ "proj4": "2.8.1",
37
38
  "request": "2.88.2",
38
- "tileserver-gl-styles": "2.0.0",
39
- "sanitize-filename": "1.6.3"
39
+ "sanitize-filename": "1.6.3",
40
+ "tileserver-gl-styles": "2.0.0"
40
41
  },
41
42
  "keywords": [
42
43
  "maptiler",
@@ -15,11 +15,14 @@ import sanitize from 'sanitize-filename';
15
15
  import SphericalMercator from '@mapbox/sphericalmercator';
16
16
  import mlgl from '@maplibre/maplibre-gl-native';
17
17
  import MBTiles from '@mapbox/mbtiles';
18
+ import polyline from '@mapbox/polyline';
18
19
  import proj4 from 'proj4';
19
20
  import request from 'request';
20
21
  import { getFontsPbf, getTileUrls, fixTileJSONCenter } from './utils.js';
21
22
 
22
23
  const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+.?\\d+)';
24
+ const PATH_PATTERN =
25
+ /^((fill|stroke|width)\:[^\|]+\|)*((enc:.+)|((-?\d+\.?\d*,-?\d+\.?\d*\|)+(-?\d+\.?\d*,-?\d+\.?\d*)))/;
23
26
  const httpTester = /^(http(s)?:)?\/\//;
24
27
 
25
28
  const mercator = new SphericalMercator();
@@ -147,47 +150,67 @@ const parseCoordinates = (coordinatePair, query, transformer) => {
147
150
  * @param {Function} transformer Optional transform function.
148
151
  */
149
152
  const extractPathsFromQuery = (query, transformer) => {
153
+ // Initiate paths array
154
+ const paths = [];
150
155
  // Return an empty list if no paths have been provided
151
- if (!query.path) {
152
- return [];
156
+ if ('path' in query && !query.path) {
157
+ return paths;
153
158
  }
154
-
155
- const paths = [];
156
-
157
- // Check if multiple paths have been provided and mimic a list if it's a
158
- // single path.
159
- const providedPaths = Array.isArray(query.path) ? query.path : [query.path];
160
-
161
- // Iterate through paths, parse and validate them
162
- for (const provided_path of providedPaths) {
163
- const currentPath = [];
164
-
165
- // Extract coordinate-list from path
166
- const pathParts = (provided_path || '').split('|');
167
-
168
- // Iterate through coordinate-list, parse the coordinates and validate them
169
- for (const pair of pathParts) {
170
- // Extract coordinates from coordinate pair
171
- const pairParts = pair.split(',');
172
-
173
- // Ensure we have two coordinates
174
- if (pairParts.length === 2) {
175
- const pair = parseCoordinates(pairParts, query, transformer);
176
-
177
- // Ensure coordinates could be parsed and skip them if not
178
- if (pair === null) {
179
- continue;
159
+ // Parse paths provided via path query argument
160
+ if ('path' in query) {
161
+ const providedPaths = Array.isArray(query.path) ? query.path : [query.path];
162
+ // Iterate through paths, parse and validate them
163
+ for (const providedPath of providedPaths) {
164
+ // Logic for pushing coords to path when path includes google polyline
165
+ if (
166
+ providedPath.includes('enc:') &&
167
+ PATH_PATTERN.test(decodeURIComponent(providedPath))
168
+ ) {
169
+ const encodedPaths = providedPath.split(',');
170
+ for (const path of encodedPaths) {
171
+ const line = path
172
+ .split('|')
173
+ .filter(
174
+ (x) =>
175
+ !x.startsWith('fill') &&
176
+ !x.startsWith('stroke') &&
177
+ !x.startsWith('width'),
178
+ )
179
+ .join('')
180
+ .replace('enc:', '');
181
+ const coords = polyline.decode(line).map(([lat, lng]) => [lng, lat]);
182
+ paths.push(coords);
180
183
  }
184
+ } else {
185
+ // Iterate through paths, parse and validate them
186
+ const currentPath = [];
187
+
188
+ // Extract coordinate-list from path
189
+ const pathParts = (providedPath || '').split('|');
190
+
191
+ // Iterate through coordinate-list, parse the coordinates and validate them
192
+ for (const pair of pathParts) {
193
+ // Extract coordinates from coordinate pair
194
+ const pairParts = pair.split(',');
195
+ // Ensure we have two coordinates
196
+ if (pairParts.length === 2) {
197
+ const pair = parseCoordinates(pairParts, query, transformer);
198
+
199
+ // Ensure coordinates could be parsed and skip them if not
200
+ if (pair === null) {
201
+ continue;
202
+ }
181
203
 
182
- // Add the coordinate-pair to the current path if they are valid
183
- currentPath.push(pair);
204
+ // Add the coordinate-pair to the current path if they are valid
205
+ currentPath.push(pair);
206
+ }
207
+ }
208
+ // Extend list of paths with current path if it contains coordinates
209
+ if (currentPath.length) {
210
+ paths.push(currentPath);
211
+ }
184
212
  }
185
213
  }
186
-
187
- // Extend list of paths with current path if it contains coordinates
188
- if (currentPath.length) {
189
- paths.push(currentPath);
190
- }
191
214
  }
192
215
  return paths;
193
216
  };
@@ -422,65 +445,109 @@ const drawMarkers = async (ctx, markers, z) => {
422
445
  * @param {number} z Map zoom level.
423
446
  */
424
447
  const drawPath = (ctx, path, query, z) => {
425
- if (!path || path.length < 2) {
426
- return null;
427
- }
448
+ const renderPath = (splitPaths) => {
449
+ if (!path || path.length < 2) {
450
+ return null;
451
+ }
428
452
 
429
- ctx.beginPath();
453
+ ctx.beginPath();
430
454
 
431
- // Transform coordinates to pixel on canvas and draw lines between points
432
- for (const pair of path) {
433
- const px = precisePx(pair, z);
434
- ctx.lineTo(px[0], px[1]);
435
- }
455
+ // Transform coordinates to pixel on canvas and draw lines between points
456
+ for (const pair of path) {
457
+ const px = precisePx(pair, z);
458
+ ctx.lineTo(px[0], px[1]);
459
+ }
436
460
 
437
- // Check if first coordinate matches last coordinate
438
- if (
439
- path[0][0] === path[path.length - 1][0] &&
440
- path[0][1] === path[path.length - 1][1]
441
- ) {
442
- ctx.closePath();
443
- }
461
+ // Check if first coordinate matches last coordinate
462
+ if (
463
+ path[0][0] === path[path.length - 1][0] &&
464
+ path[0][1] === path[path.length - 1][1]
465
+ ) {
466
+ ctx.closePath();
467
+ }
444
468
 
445
- // Optionally fill drawn shape with a rgba color from query
446
- if (query.fill !== undefined) {
447
- ctx.fillStyle = query.fill;
448
- ctx.fill();
449
- }
469
+ // Optionally fill drawn shape with a rgba color from query
470
+ const pathHasFill =
471
+ splitPaths.filter((x) => x.startsWith('fill')).length > 0;
472
+ if (query.fill !== undefined || pathHasFill) {
473
+ if ('fill' in query) {
474
+ ctx.fillStyle = query.fill || 'rgba(255,255,255,0.4)';
475
+ }
476
+ if (pathHasFill) {
477
+ ctx.fillStyle = splitPaths
478
+ .find((x) => x.startsWith('fill:'))
479
+ .replace('fill:', '');
480
+ }
481
+ ctx.fill();
482
+ }
450
483
 
451
- // Get line width from query and fall back to 1 if not provided
452
- const lineWidth = query.width !== undefined ? parseFloat(query.width) : 1;
453
-
454
- // Ensure line width is valid
455
- if (lineWidth > 0) {
456
- // Get border width from query and fall back to 10% of line width
457
- const borderWidth =
458
- query.borderwidth !== undefined
459
- ? parseFloat(query.borderwidth)
460
- : lineWidth * 0.1;
461
-
462
- // Set rendering style for the start and end points of the path
463
- // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap
464
- ctx.lineCap = query.linecap || 'butt';
465
-
466
- // Set rendering style for overlapping segments of the path with differing directions
467
- // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin
468
- ctx.lineJoin = query.linejoin || 'miter';
469
-
470
- // In order to simulate a border we draw the path two times with the first
471
- // beeing the wider border part.
472
- if (query.border !== undefined && borderWidth > 0) {
473
- // We need to double the desired border width and add it to the line width
474
- // in order to get the desired border on each side of the line.
475
- ctx.lineWidth = lineWidth + borderWidth * 2;
476
- // Set border style as rgba
477
- ctx.strokeStyle = query.border;
478
- ctx.stroke();
484
+ // Get line width from query and fall back to 1 if not provided
485
+ const pathHasWidth =
486
+ splitPaths.filter((x) => x.startsWith('width')).length > 0;
487
+ if (query.width !== undefined || pathHasWidth) {
488
+ let lineWidth = 1;
489
+ // Get line width from query
490
+ if ('width' in query) {
491
+ lineWidth = Number(query.width);
492
+ }
493
+ // Get line width from path in query
494
+ if (pathHasWidth) {
495
+ lineWidth = Number(
496
+ splitPaths.find((x) => x.startsWith('width:')).replace('width:', ''),
497
+ );
498
+ }
499
+ // Get border width from query and fall back to 10% of line width
500
+ const borderWidth =
501
+ query.borderwidth !== undefined
502
+ ? parseFloat(query.borderwidth)
503
+ : lineWidth * 0.1;
504
+
505
+ // Set rendering style for the start and end points of the path
506
+ // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap
507
+ ctx.lineCap = query.linecap || 'butt';
508
+
509
+ // Set rendering style for overlapping segments of the path with differing directions
510
+ // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin
511
+ ctx.lineJoin = query.linejoin || 'miter';
512
+
513
+ // In order to simulate a border we draw the path two times with the first
514
+ // beeing the wider border part.
515
+ if (query.border !== undefined && borderWidth > 0) {
516
+ // We need to double the desired border width and add it to the line width
517
+ // in order to get the desired border on each side of the line.
518
+ ctx.lineWidth = lineWidth + borderWidth * 2;
519
+ // Set border style as rgba
520
+ ctx.strokeStyle = query.border;
521
+ ctx.stroke();
522
+ }
523
+ ctx.lineWidth = lineWidth;
479
524
  }
480
525
 
481
- ctx.lineWidth = lineWidth;
482
- ctx.strokeStyle = query.stroke || 'rgba(0,64,255,0.7)';
526
+ const pathHasStroke =
527
+ splitPaths.filter((x) => x.startsWith('stroke')).length > 0;
528
+ if (query.stroke !== undefined || pathHasStroke) {
529
+ if ('stroke' in query) {
530
+ ctx.strokeStyle = query.stroke;
531
+ }
532
+ // Path Width gets higher priority
533
+ if (pathHasWidth) {
534
+ ctx.strokeStyle = splitPaths
535
+ .find((x) => x.startsWith('stroke:'))
536
+ .replace('stroke:', '');
537
+ }
538
+ } else {
539
+ ctx.strokeStyle = 'rgba(0,64,255,0.7)';
540
+ }
483
541
  ctx.stroke();
542
+ };
543
+
544
+ // Check if path in query is valid
545
+ if (Array.isArray(query.path)) {
546
+ for (let i = 0; i < query.path.length; i += 1) {
547
+ renderPath(decodeURIComponent(query.path.at(i)).split('|'));
548
+ }
549
+ } else {
550
+ renderPath(decodeURIComponent(query.path).split('|'));
484
551
  }
485
552
  };
486
553
 
@@ -615,6 +682,7 @@ export const serve_rendered = {
615
682
  ) {
616
683
  return res.status(400).send('Invalid center');
617
684
  }
685
+
618
686
  if (
619
687
  Math.min(width, height) <= 0 ||
620
688
  Math.max(width, height) * scale > (options.maxSize || 2048) ||
@@ -623,6 +691,7 @@ export const serve_rendered = {
623
691
  ) {
624
692
  return res.status(400).send('Invalid size');
625
693
  }
694
+
626
695
  if (format === 'png' || format === 'webp') {
627
696
  } else if (format === 'jpg' || format === 'jpeg') {
628
697
  format = 'jpeg';
@@ -630,8 +699,9 @@ export const serve_rendered = {
630
699
  return res.status(400).send('Invalid format');
631
700
  }
632
701
 
702
+ const tileMargin = Math.max(options.tileMargin || 0, 0);
633
703
  let pool;
634
- if (opt_mode === 'tile') {
704
+ if (opt_mode === 'tile' && tileMargin === 0) {
635
705
  pool = item.map.renderers[scale];
636
706
  } else {
637
707
  pool = item.map.renderers_static[scale];
@@ -646,12 +716,12 @@ export const serve_rendered = {
646
716
  width: width,
647
717
  height: height,
648
718
  };
719
+
649
720
  if (z === 0) {
650
721
  params.width *= 2;
651
722
  params.height *= 2;
652
723
  }
653
724
 
654
- const tileMargin = Math.max(options.tileMargin || 0, 0);
655
725
  if (z > 2 && tileMargin > 0) {
656
726
  params.width += tileMargin * 2;
657
727
  params.height += tileMargin * 2;