This is an automated email from the ASF dual-hosted git repository.

bchapuis pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-baremaps.git


The following commit(s) were added to refs/heads/main by this push:
     new b5f598c3 Add renderer and integration tests for the basemap (#659)
b5f598c3 is described below

commit b5f598c32386a7afdb16061021f3defebc0c7e52
Author: Leonard <[email protected]>
AuthorDate: Sun May 21 20:15:15 2023 +0200

    Add renderer and integration tests for the basemap (#659)
---
 baremaps-renderer/.gitignore                       |    2 +
 baremaps-renderer/.prettierignore                  |    3 +
 baremaps-renderer/.prettierrc.json                 |    4 +
 baremaps-renderer/README.md                        |   78 +
 baremaps-renderer/assets/report-template.html      |  334 ++++
 baremaps-renderer/declaration.d.ts                 |   13 +
 baremaps-renderer/package-lock.json                | 1722 ++++++++++++++++++++
 baremaps-renderer/package.json                     |   34 +
 baremaps-renderer/src/index.ts                     |  129 ++
 baremaps-renderer/src/lib/browserPool.ts           |   86 +
 .../src/lib/maplibreBrowserHelpers.ts              |  128 ++
 baremaps-renderer/src/lib/reportGenerator.ts       |  124 ++
 baremaps-renderer/src/lib/runnableTask.ts          |   24 +
 baremaps-renderer/src/lib/test.ts                  |  178 ++
 baremaps-renderer/src/lib/testManager.ts           |   90 +
 baremaps-renderer/src/lib/testsLogger.ts           |  112 ++
 baremaps-renderer/src/types/index.ts               |   36 +
 baremaps-renderer/tsconfig.json                    |   20 +
 basemap/tests/.gitignore                           |    2 +
 basemap/tests/integration/lausanne/metadata.json   |    6 +
 20 files changed, 3125 insertions(+)

diff --git a/baremaps-renderer/.gitignore b/baremaps-renderer/.gitignore
new file mode 100644
index 00000000..9e87c054
--- /dev/null
+++ b/baremaps-renderer/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+bin
\ No newline at end of file
diff --git a/baremaps-renderer/.prettierignore 
b/baremaps-renderer/.prettierignore
new file mode 100644
index 00000000..e5f8ad9a
--- /dev/null
+++ b/baremaps-renderer/.prettierignore
@@ -0,0 +1,3 @@
+node_modules
+package-lock.json
+bin
diff --git a/baremaps-renderer/.prettierrc.json 
b/baremaps-renderer/.prettierrc.json
new file mode 100644
index 00000000..6e778b4f
--- /dev/null
+++ b/baremaps-renderer/.prettierrc.json
@@ -0,0 +1,4 @@
+{
+  "trailingComma": "all",
+  "singleQuote": true
+}
diff --git a/baremaps-renderer/README.md b/baremaps-renderer/README.md
new file mode 100644
index 00000000..55bc8a98
--- /dev/null
+++ b/baremaps-renderer/README.md
@@ -0,0 +1,78 @@
+<!--
+  Licensed under the Apache License, Version 2.0 (the "License"); you may not 
use this file except
+  in compliance with the License. You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software 
distributed under the License
+  is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
KIND, either express
+  or implied. See the License for the specific language governing permissions 
and limitations under
+  the License.
+  -->
+
+<h1>Baremaps Renderer</h1>
+
+- [Installation](#installation)
+- [Usage](#usage)
+  - [Running the Tests](#running-the-tests)
+- [Creating a New Test](#creating-a-new-test)
+
+## Installation
+
+First, install the required dependencies:
+
+```bash
+npm install
+```
+
+Then, build the project:
+
+```bash
+npm run build
+```
+
+You can then link the script to your path in order to use it as a command line 
tool:
+
+```bash
+npm link
+```
+
+## Usage
+
+In order to get a list of the available commands, run:
+
+```bash
+baremaps-renderer --help
+```
+
+### Running the Tests
+
+Within a directory containing a `tests` folder, you can run the tests by 
running:
+
+```bash
+baremaps-renderer run -s <styleUrl>
+```
+
+After the tests are run, a report is generated `tests/report.html`. You can 
view the report by opening the file in your browser or by running:
+
+```bash
+baremaps-renderer report --open
+```
+
+## Creating a New Test
+
+Within a directory containing a `tests` folder, you can add a new test by 
creating a folder in the `tests/integration` directory. Each test is a folder 
containing a metadata file `metadata.json`. The metadata file is as follows:
+
+```json
+{
+  "width": 512,
+  "height": 512,
+  "center": [6.6323, 46.5197],
+  "zoom": 14
+}
+```
+
+- `width:` the width of the image in pixels
+- `height:` the height of the image in pixels
+- `center:` the center of the map longitude, latitude
+- `zoom:` the zoom level of the map
diff --git a/baremaps-renderer/assets/report-template.html 
b/baremaps-renderer/assets/report-template.html
new file mode 100644
index 00000000..696931bb
--- /dev/null
+++ b/baremaps-renderer/assets/report-template.html
@@ -0,0 +1,334 @@
+<!--
+  Licensed under the Apache License, Version 2.0 (the "License"); you may not 
use this file except
+  in compliance with the License. You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software 
distributed under the License
+  is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
KIND, either express
+  or implied. See the License for the specific language governing permissions 
and limitations under
+  the License.
+  -->
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Baremaps Renderer</title>
+    <style>
+      :root {
+        --color-pass: rgb(40, 156, 40);
+        --color-pass-transparent: rgba(40, 156, 40, 0.05);
+        --color-error: rgb(255, 59, 59);
+        --color-error-transparent: rgba(255, 59, 59, 0.05);
+      }
+
+      body {
+        font-family: sans-serif;
+        padding: 4rem 8rem;
+      }
+
+      @media (max-width: 768px) {
+        body {
+          padding: 4rem 0.5rem;
+        }
+
+        .results {
+          gap: 0.5rem !important;
+        }
+      }
+
+      .title {
+        margin-bottom: 3rem;
+      }
+
+      h1 {
+        font-size: 3rem;
+        margin-top: 0;
+        margin-bottom: 1rem;
+      }
+
+      h2 {
+        font-size: 1.5rem;
+        margin-top: 0;
+        margin-bottom: 1rem;
+        text-transform: uppercase;
+        font-weight: 500;
+      }
+
+      h3 {
+        font-size: 2rem;
+        margin: 1rem 0;
+      }
+
+      h4 {
+        font-size: 1.25rem;
+        margin-bottom: 0;
+      }
+
+      h4.fail {
+        margin-top: 0;
+        text-transform: uppercase;
+        color: var(--color-error);
+      }
+
+      h4.pass {
+        margin-top: 0;
+        text-transform: uppercase;
+        color: var(--color-pass);
+      }
+
+      a {
+        position: relative;
+        text-decoration: underline;
+        color: inherit;
+      }
+
+      pre {
+        font-size: 1.25rem;
+      }
+
+      button {
+        border: none;
+        background-color: transparent;
+        cursor: pointer;
+        font-size: 1rem;
+        padding: 0.25rem 0.5rem;
+        border-radius: 0.25rem;
+        box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.075);
+      }
+
+      .sort {
+        display: flex;
+        align-items: center;
+        gap: 0.5rem;
+        margin-bottom: 1rem;
+      }
+
+      .sort p {
+        margin-right: 0.5rem;
+      }
+
+      .sort button.active {
+        box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
+        background-color: rgba(0, 0, 0, 0.075);
+      }
+
+      .results {
+        display: flex;
+        flex-direction: column;
+        gap: 2rem;
+      }
+
+      .summary {
+        border-left: 3px solid black;
+        padding: 1rem 2rem;
+        background-color: rgba(0, 0, 0, 0.05);
+      }
+
+      .summary h4 {
+        text-transform: uppercase;
+        margin-top: 1rem;
+      }
+
+      .p-pass {
+        color: var(--color-pass);
+        font-weight: bold;
+      }
+
+      .p-fail {
+        color: var(--color-error);
+        font-weight: bold;
+      }
+
+      .result {
+        padding: 1rem 2rem;
+      }
+
+      .result.fail {
+        border-left: 3px solid var(--color-error);
+        background-color: var(--color-error-transparent);
+      }
+
+      .result.pass {
+        border-left: 3px solid var(--color-pass);
+        background-color: var(--color-pass-transparent);
+      }
+
+      pre {
+        margin: 0;
+      }
+
+      .images {
+        display: grid;
+        grid-template-columns: 1fr 1fr 1fr;
+        gap: 1rem;
+      }
+
+      img {
+        width: 100%;
+        aspect-ratio: 1;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="title">
+      <h1>Baremaps Renderer</h1>
+      <h2>Integration Testing Report</h2>
+    </div>
+    <div>
+      <!-- Sort by test status, test diff, test name -->
+      <div class="sort">
+        <p><strong>Sort by:</strong></p>
+        <button onClick="sortBy(this, 'status')">Test status</button>
+        <button onClick="sortBy(this, 'diff')">Test diff</button>
+        <button onClick="sortBy(this, 'name')">Test name</button>
+      </div>
+    </div>
+    <!-- Test results container (incl. the summary) -->
+    <div class="results" id="testResults"></div>
+  </body>
+  <script>
+    /** Template for the test summary */
+    const SUMMARY_TEMPLATE = `<div class="summary">
+<h4>Summary</h4>
+<p>Out of <strong>{{ TOT_TESTS }}</strong> tests:</p>
+<ul>
+  <li><span class="p-pass">{{ TOT_PASS_TESTS }} tests passed</span></li>
+  <li><span class="p-fail">{{ TOT_FAIL_TESTS }} tests failed</span></li>
+</ul>
+</div>`;
+
+    /**
+     * Helper function to get the test template
+     *
+     * @param {boolean} passed - Whether the test passed or not
+     * @param {number} index - The index of the test
+     * @returns {string} - The test template
+     */
+    const getTestTemplate = (passed, index) => {
+      const loading = index > 2 ? 'lazy' : 'eager';
+      return `<div class="result ${passed ? 'pass' : 'fail'}">
+  <h4 class="${passed ? 'pass' : 'fail'}">${passed ? 'PASSED' : 'FAILED'}</h4>
+  <h3>{{ TEST_NAME }}</h3>
+  <p>
+      {{ TEST_PATH }}
+  </p>
+  <pre>
+      <code>
+{{ METADATA }}</code>
+  </pre>
+  <div class="images">
+      <h4>Expected</h4>
+      <h4>Actual</h4>
+      <h4>Difference ({{ DIFF }})</h4>
+      <img src="{{ EXPECTED_IMG_PATH }}" loading="${loading}" />
+      <img src="{{ ACTUAL_IMG_PATH }}" loading="${loading}" />
+      <img src="{{ DIFF_IMG_PATH }}" loading="${loading}" />
+  </div>
+</div>`;
+    };
+
+    const testData = JSON.parse(`{{ TESTS_DATA }}`);
+
+    /**
+     * Updates the sort parameters and re-renders the test results
+     *
+     * @param {HTMLElement} el - The element that was clicked
+     * @param {string} key - The key to sort by
+     */
+    const sortBy = (el, key) => {
+      if (!sortedBy[key].active) {
+        el.classList.add('active');
+        el.innerText = `${el.innerText} ▲`;
+        sortedBy[key].asc = true;
+        sortedBy[key].active = true;
+      } else if (sortedBy[key].active && sortedBy[key].asc) {
+        el.classList.remove('asc');
+        el.innerText = `${el.innerText.replace(' ▲', '')} ▼`;
+        sortedBy[key].asc = false;
+      } else if (sortedBy[key].active && !sortedBy[key].asc) {
+        el.classList.remove('active');
+        el.innerText = el.innerText.replace(' ▼', '');
+        sortedBy[key].active = false;
+      }
+      render();
+    };
+
+    let sortedBy = {
+      status: {
+        asc: false,
+        active: false,
+      },
+      diff: {
+        asc: false,
+        active: false,
+      },
+      name: {
+        asc: false,
+        active: false,
+      },
+    };
+
+    /** Render the test results */
+    const render = () => {
+      const testResultsDiv = document.getElementById('testResults');
+      testResultsDiv.innerHTML = '';
+      const summary = document.createElement('div');
+      summary.innerHTML = SUMMARY_TEMPLATE.replace(
+        '{{ TOT_TESTS }}',
+        testData.length,
+      )
+        .replace(
+          '{{ TOT_PASS_TESTS }}',
+          testData.filter((test) => test.success).length,
+        )
+        .replace(
+          '{{ TOT_FAIL_TESTS }}',
+          testData.filter((test) => !test.success).length,
+        );
+      testResultsDiv.appendChild(summary);
+      const sortedTestData = [...testData];
+      // sort the test data
+      if (sortedBy.name.active) {
+        sortedBy.name.asc
+          ? sortedTestData.sort((a, b) => (a.name > b.name ? 1 : -1))
+          : sortedTestData.sort((a, b) => (a.name < b.name ? 1 : -1));
+      }
+      if (sortedBy.diff.active) {
+        sortedBy.diff.asc
+          ? sortedTestData.sort((a, b) => a.diff - b.diff)
+          : sortedTestData.sort((a, b) => b.diff - a.diff);
+      }
+      if (sortedBy.status.active) {
+        sortedBy.status.asc
+          ? sortedTestData.sort((a, b) =>
+              a.success === b.success ? 0 : a.success ? -1 : 1,
+            )
+          : sortedTestData.sort((a, b) =>
+              a.success === b.success ? 0 : a.success ? 1 : -1,
+            );
+      }
+
+      sortedTestData.forEach((test, index) => {
+        const testDiv = document.createElement('div');
+        testDiv.innerHTML = getTestTemplate(test.success, index);
+        testDiv.innerHTML = testDiv.innerHTML
+          .replace('{{ TEST_NAME }}', test.name)
+          .replace('{{ TEST_PATH }}', test.path)
+          .replace('{{ METADATA }}', JSON.stringify(test.metadata, null, 4))
+          .replace('{{ DIFF }}', test.diff)
+          .replace(
+            '{{ EXPECTED_IMG_PATH }}',
+            test.expectedImagePath,
+          )
+          .replace(
+            '{{ ACTUAL_IMG_PATH }}',
+            test.actualImagePath,
+          )
+          .replace('{{ DIFF_IMG_PATH }}', test.diffImagePath);
+        testResultsDiv.appendChild(testDiv);
+      });
+    };
+
+    render();
+  </script>
+</html>
diff --git a/baremaps-renderer/declaration.d.ts 
b/baremaps-renderer/declaration.d.ts
new file mode 100644
index 00000000..42b0c2c3
--- /dev/null
+++ b/baremaps-renderer/declaration.d.ts
@@ -0,0 +1,13 @@
+/**
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not 
use this file except
+ in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software 
distributed under the License
+ is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
KIND, either express
+ or implied. See the License for the specific language governing permissions 
and limitations under
+ the License.
+ **/
+
+declare module 'st';
diff --git a/baremaps-renderer/package-lock.json 
b/baremaps-renderer/package-lock.json
new file mode 100644
index 00000000..94f405dc
--- /dev/null
+++ b/baremaps-renderer/package-lock.json
@@ -0,0 +1,1722 @@
+{
+  "name": "baremaps-renderer",
+  "version": "0.0.2",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "baremaps-renderer",
+      "version": "0.0.2",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "chalk": "^4",
+        "commander": "^10.0.1",
+        "http": "^0.0.1-security",
+        "maplibre-gl": "^2.4.0",
+        "pixelmatch": "^5.3.0",
+        "pngjs": "^7.0.0",
+        "puppeteer": "^20.2.0",
+        "st": "^3.0.0"
+      },
+      "bin": {
+        "baremaps-renderer": "bin/index.js"
+      },
+      "devDependencies": {
+        "@types/node": "^20.1.4",
+        "@types/pixelmatch": "^5.2.4",
+        "@types/pngjs": "^6.0.1",
+        "@types/serve-handler": "^6.1.1",
+        "copyfiles": "^2.4.1",
+        "prettier": "^2.8.8",
+        "typescript": "^5.0.4"
+      }
+    },
+    "node_modules/@babel/code-frame": {
+      "version": "7.21.4",
+      "resolved": 
"https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz";,
+      "integrity": 
"sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==",
+      "dependencies": {
+        "@babel/highlight": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.19.1",
+      "resolved": 
"https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz";,
+      "integrity": 
"sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/highlight": {
+      "version": "7.18.6",
+      "resolved": 
"https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz";,
+      "integrity": 
"sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.18.6",
+        "chalk": "^2.0.0",
+        "js-tokens": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/highlight/node_modules/chalk": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz";,
+      "integrity": 
"sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+      "dependencies": {
+        "ansi-styles": "^3.2.1",
+        "escape-string-regexp": "^1.0.5",
+        "supports-color": "^5.3.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/@mapbox/geojson-rewind": {
+      "version": "0.5.2",
+      "resolved": 
"https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz";,
+      "integrity": 
"sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==",
+      "dependencies": {
+        "get-stream": "^6.0.1",
+        "minimist": "^1.2.6"
+      },
+      "bin": {
+        "geojson-rewind": "geojson-rewind"
+      }
+    },
+    "node_modules/@mapbox/geojson-rewind/node_modules/get-stream": {
+      "version": "6.0.1",
+      "resolved": 
"https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz";,
+      "integrity": 
"sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus";
+      }
+    },
+    "node_modules/@mapbox/jsonlint-lines-primitives": {
+      "version": "2.0.2",
+      "resolved": 
"https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz";,
+      "integrity": 
"sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/@mapbox/mapbox-gl-supported": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-2.0.1.tgz";,
+      "integrity": 
"sha512-HP6XvfNIzfoMVfyGjBckjiAOQK9WfX0ywdLubuPMPv+Vqf5fj0uCbgBQYpiqcWZT6cbyyRnTSXDheT1ugvF6UQ=="
+    },
+    "node_modules/@mapbox/point-geometry": {
+      "version": "0.1.0",
+      "resolved": 
"https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz";,
+      "integrity": 
"sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ=="
+    },
+    "node_modules/@mapbox/tiny-sdf": {
+      "version": "2.0.6",
+      "resolved": 
"https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz";,
+      "integrity": 
"sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA=="
+    },
+    "node_modules/@mapbox/unitbezier": {
+      "version": "0.0.1",
+      "resolved": 
"https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz";,
+      "integrity": 
"sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw=="
+    },
+    "node_modules/@mapbox/vector-tile": {
+      "version": "1.3.1",
+      "resolved": 
"https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz";,
+      "integrity": 
"sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==",
+      "dependencies": {
+        "@mapbox/point-geometry": "~0.1.0"
+      }
+    },
+    "node_modules/@mapbox/whoots-js": {
+      "version": "3.1.0",
+      "resolved": 
"https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz";,
+      "integrity": 
"sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@puppeteer/browsers": {
+      "version": "1.2.0",
+      "resolved": 
"https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.2.0.tgz";,
+      "integrity": 
"sha512-F2ygRTaNKq2HQQGsvypvy2S/Dg7aqqp2zxv4uolkxxTQvdbYfI0DcLPFNdqenaC+rZX5ldSPs/s39yAPpTVZ0A==",
+      "dependencies": {
+        "debug": "4.3.4",
+        "extract-zip": "2.0.1",
+        "http-proxy-agent": "5.0.0",
+        "https-proxy-agent": "5.0.1",
+        "progress": "2.0.3",
+        "proxy-from-env": "1.1.0",
+        "tar-fs": "2.1.1",
+        "unbzip2-stream": "1.4.3",
+        "yargs": "17.7.1"
+      },
+      "bin": {
+        "browsers": "lib/cjs/main-cli.js"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      },
+      "peerDependencies": {
+        "typescript": ">= 4.7.4"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@tootallnate/once": {
+      "version": "2.0.0",
+      "resolved": 
"https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz";,
+      "integrity": 
"sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@types/geojson": {
+      "version": "7946.0.10",
+      "resolved": 
"https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz";,
+      "integrity": 
"sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA=="
+    },
+    "node_modules/@types/mapbox__point-geometry": {
+      "version": "0.1.2",
+      "resolved": 
"https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.2.tgz";,
+      "integrity": 
"sha512-D0lgCq+3VWV85ey1MZVkE8ZveyuvW5VAfuahVTQRpXFQTxw03SuIf1/K4UQ87MMIXVKzpFjXFiFMZzLj2kU+iA=="
+    },
+    "node_modules/@types/mapbox__vector-tile": {
+      "version": "1.3.0",
+      "resolved": 
"https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.0.tgz";,
+      "integrity": 
"sha512-kDwVreQO5V4c8yAxzZVQLE5tyWF+IPToAanloQaSnwfXmIcJ7cyOrv8z4Ft4y7PsLYmhWXmON8MBV8RX0Rgr8g==",
+      "dependencies": {
+        "@types/geojson": "*",
+        "@types/mapbox__point-geometry": "*",
+        "@types/pbf": "*"
+      }
+    },
+    "node_modules/@types/node": {
+      "version": "20.1.4",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.4.tgz";,
+      "integrity": 
"sha512-At4pvmIOki8yuwLtd7BNHl3CiWNbtclUbNtScGx4OHfBd4/oWoJC8KRCIxXwkdndzhxOsPXihrsOoydxBjlE9Q==",
+      "devOptional": true
+    },
+    "node_modules/@types/pbf": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.2.tgz";,
+      "integrity": 
"sha512-EDrLIPaPXOZqDjrkzxxbX7UlJSeQVgah3i0aA4pOSzmK9zq3BIh7/MZIQxED7slJByvKM4Gc6Hypyu2lJzh3SQ=="
+    },
+    "node_modules/@types/pixelmatch": {
+      "version": "5.2.4",
+      "resolved": 
"https://registry.npmjs.org/@types/pixelmatch/-/pixelmatch-5.2.4.tgz";,
+      "integrity": 
"sha512-HDaSHIAv9kwpMN7zlmwfTv6gax0PiporJOipcrGsVNF3Ba+kryOZc0Pio5pn6NhisgWr7TaajlPEKTbTAypIBQ==",
+      "dev": true,
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/pngjs": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.1.tgz";,
+      "integrity": 
"sha512-J39njbdW1U/6YyVXvC9+1iflZghP8jgRf2ndYghdJb5xL49LYDB+1EuAxfbuJ2IBbWIL3AjHPQhgaTxT3YaYeg==",
+      "dev": true,
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/serve-handler": {
+      "version": "6.1.1",
+      "resolved": 
"https://registry.npmjs.org/@types/serve-handler/-/serve-handler-6.1.1.tgz";,
+      "integrity": 
"sha512-bIwSmD+OV8w0t2e7EWsuQYlGoS1o5aEdVktgkXaa43Zm0qVWi21xaSRb3DQA1UXD+DJ5bRq1Rgu14ZczB+CjIQ==",
+      "dev": true,
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/yauzl": {
+      "version": "2.10.0",
+      "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz";,
+      "integrity": 
"sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==",
+      "optional": true,
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/agent-base": {
+      "version": "6.0.2",
+      "resolved": 
"https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz";,
+      "integrity": 
"sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "dependencies": {
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": 
"https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz";,
+      "integrity": 
"sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "3.2.1",
+      "resolved": 
"https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz";,
+      "integrity": 
"sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+      "dependencies": {
+        "color-convert": "^1.9.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz";,
+      "integrity": 
"sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
+    },
+    "node_modules/async-cache": {
+      "version": "1.1.0",
+      "resolved": 
"https://registry.npmjs.org/async-cache/-/async-cache-1.1.0.tgz";,
+      "integrity": 
"sha512-YDQc4vBn5NFhY6g6HhVshyi3Fy9+SQ5ePnE7JLDJn1DoL+i7ER+vMwtTNOYk9leZkYMnOwpBCWqyLDPw8Aig8g==",
+      "deprecated": "No longer maintained. Use 
[lru-cache](http://npm.im/lru-cache) version 7.6 or higher, and provide an 
asynchronous `fetchMethod` option.",
+      "dependencies": {
+        "lru-cache": "^4.0.0"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": 
"https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz";,
+      "integrity": 
"sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true
+    },
+    "node_modules/base64-js": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz";,
+      "integrity": 
"sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross";
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross";
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support";
+        }
+      ]
+    },
+    "node_modules/bl": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz";,
+      "integrity": 
"sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+      "dependencies": {
+        "buffer": "^5.5.0",
+        "inherits": "^2.0.4",
+        "readable-stream": "^3.4.0"
+      }
+    },
+    "node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": 
"https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz";,
+      "integrity": 
"sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/buffer": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz";,
+      "integrity": 
"sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross";
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross";
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support";
+        }
+      ],
+      "dependencies": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.1.13"
+      }
+    },
+    "node_modules/buffer-crc32": {
+      "version": "0.2.13",
+      "resolved": 
"https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz";,
+      "integrity": 
"sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz";,
+      "integrity": 
"sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz";,
+      "integrity": 
"sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1";
+      }
+    },
+    "node_modules/chalk/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": 
"https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz";,
+      "integrity": 
"sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1";
+      }
+    },
+    "node_modules/chalk/node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz";,
+      "integrity": 
"sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/chalk/node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": 
"https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz";,
+      "integrity": 
"sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+    },
+    "node_modules/chalk/node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz";,
+      "integrity": 
"sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/chalk/node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": 
"https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz";,
+      "integrity": 
"sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/chownr": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz";,
+      "integrity": 
"sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
+    },
+    "node_modules/chromium-bidi": {
+      "version": "0.4.7",
+      "resolved": 
"https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.7.tgz";,
+      "integrity": 
"sha512-6+mJuFXwTMU6I3vYLs6IL8A1DyQTPjCfIL971X0aMPVGRbGnNfl6i6Cl0NMbxi2bRYLGESt9T2ZIMRM5PAEcIQ==",
+      "dependencies": {
+        "mitt": "3.0.0"
+      },
+      "peerDependencies": {
+        "devtools-protocol": "*"
+      }
+    },
+    "node_modules/cliui": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz";,
+      "integrity": 
"sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.1",
+        "wrap-ansi": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "1.9.3",
+      "resolved": 
"https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz";,
+      "integrity": 
"sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+      "dependencies": {
+        "color-name": "1.1.3"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.3",
+      "resolved": 
"https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz";,
+      "integrity": 
"sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
+    },
+    "node_modules/commander": {
+      "version": "10.0.1",
+      "resolved": 
"https://registry.npmjs.org/commander/-/commander-10.0.1.tgz";,
+      "integrity": 
"sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "resolved": 
"https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz";,
+      "integrity": 
"sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+      "dev": true
+    },
+    "node_modules/copyfiles": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz";,
+      "integrity": 
"sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==",
+      "dev": true,
+      "dependencies": {
+        "glob": "^7.0.5",
+        "minimatch": "^3.0.3",
+        "mkdirp": "^1.0.4",
+        "noms": "0.0.0",
+        "through2": "^2.0.1",
+        "untildify": "^4.0.0",
+        "yargs": "^16.1.0"
+      },
+      "bin": {
+        "copyfiles": "copyfiles",
+        "copyup": "copyfiles"
+      }
+    },
+    "node_modules/copyfiles/node_modules/cliui": {
+      "version": "7.0.4",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz";,
+      "integrity": 
"sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+      "dev": true,
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.0",
+        "wrap-ansi": "^7.0.0"
+      }
+    },
+    "node_modules/copyfiles/node_modules/yargs": {
+      "version": "16.2.0",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz";,
+      "integrity": 
"sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+      "dev": true,
+      "dependencies": {
+        "cliui": "^7.0.2",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.0",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^20.2.2"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/copyfiles/node_modules/yargs-parser": {
+      "version": "20.2.9",
+      "resolved": 
"https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz";,
+      "integrity": 
"sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/core-util-is": {
+      "version": "1.0.3",
+      "resolved": 
"https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz";,
+      "integrity": 
"sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+      "dev": true
+    },
+    "node_modules/cosmiconfig": {
+      "version": "8.1.3",
+      "resolved": 
"https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.3.tgz";,
+      "integrity": 
"sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==",
+      "dependencies": {
+        "import-fresh": "^3.2.1",
+        "js-yaml": "^4.1.0",
+        "parse-json": "^5.0.0",
+        "path-type": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/d-fischer";
+      }
+    },
+    "node_modules/cross-fetch": {
+      "version": "3.1.5",
+      "resolved": 
"https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz";,
+      "integrity": 
"sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
+      "dependencies": {
+        "node-fetch": "2.6.7"
+      }
+    },
+    "node_modules/csscolorparser": {
+      "version": "1.0.3",
+      "resolved": 
"https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz";,
+      "integrity": 
"sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w=="
+    },
+    "node_modules/debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz";,
+      "integrity": 
"sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "dependencies": {
+        "ms": "2.1.2"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/devtools-protocol": {
+      "version": "0.0.1120988",
+      "resolved": 
"https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1120988.tgz";,
+      "integrity": 
"sha512-39fCpE3Z78IaIPChJsP6Lhmkbf4dWXOmzLk/KFTdRkNk/0JymRIfUynDVRndV9HoDz8PyalK1UH21ST/ivwW5Q=="
+    },
+    "node_modules/earcut": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz";,
+      "integrity": 
"sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ=="
+    },
+    "node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": 
"https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz";,
+      "integrity": 
"sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+    },
+    "node_modules/end-of-stream": {
+      "version": "1.4.4",
+      "resolved": 
"https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz";,
+      "integrity": 
"sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+      "dependencies": {
+        "once": "^1.4.0"
+      }
+    },
+    "node_modules/error-ex": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz";,
+      "integrity": 
"sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+      "dependencies": {
+        "is-arrayish": "^0.2.1"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz";,
+      "integrity": 
"sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": 
"https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz";,
+      "integrity": 
"sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/extract-zip": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz";,
+      "integrity": 
"sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
+      "dependencies": {
+        "debug": "^4.1.1",
+        "get-stream": "^5.1.0",
+        "yauzl": "^2.10.0"
+      },
+      "bin": {
+        "extract-zip": "cli.js"
+      },
+      "engines": {
+        "node": ">= 10.17.0"
+      },
+      "optionalDependencies": {
+        "@types/yauzl": "^2.9.1"
+      }
+    },
+    "node_modules/fd": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/fd/-/fd-0.0.3.tgz";,
+      "integrity": 
"sha512-iAHrIslQb3U68OcMSP0kkNWabp7sSN6d2TBSb2JO3gcLJVDd4owr/hKM4SFJovFOUeeXeItjYgouEDTMWiVAnA=="
+    },
+    "node_modules/fd-slicer": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz";,
+      "integrity": 
"sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
+      "dependencies": {
+        "pend": "~1.2.0"
+      }
+    },
+    "node_modules/fs-constants": {
+      "version": "1.0.0",
+      "resolved": 
"https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz";,
+      "integrity": 
"sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
+    },
+    "node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "resolved": 
"https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz";,
+      "integrity": 
"sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+      "dev": true
+    },
+    "node_modules/geojson-vt": {
+      "version": "3.2.1",
+      "resolved": 
"https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz";,
+      "integrity": 
"sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg=="
+    },
+    "node_modules/get-caller-file": {
+      "version": "2.0.5",
+      "resolved": 
"https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz";,
+      "integrity": 
"sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "engines": {
+        "node": "6.* || 8.* || >= 10.*"
+      }
+    },
+    "node_modules/get-stream": {
+      "version": "5.2.0",
+      "resolved": 
"https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz";,
+      "integrity": 
"sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+      "dependencies": {
+        "pump": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus";
+      }
+    },
+    "node_modules/gl-matrix": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz";,
+      "integrity": 
"sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA=="
+    },
+    "node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz";,
+      "integrity": 
"sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "dev": true,
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs";
+      }
+    },
+    "node_modules/global-prefix": {
+      "version": "3.0.0",
+      "resolved": 
"https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz";,
+      "integrity": 
"sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==",
+      "dependencies": {
+        "ini": "^1.3.5",
+        "kind-of": "^6.0.2",
+        "which": "^1.3.1"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "resolved": 
"https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz";,
+      "integrity": 
"sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+      "optional": true
+    },
+    "node_modules/has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz";,
+      "integrity": 
"sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/http": {
+      "version": "0.0.1-security",
+      "resolved": "https://registry.npmjs.org/http/-/http-0.0.1-security.tgz";,
+      "integrity": 
"sha512-RnDvP10Ty9FxqOtPZuxtebw1j4L/WiqNMDtuc1YMH1XQm5TgDRaR1G9u8upL6KD1bXHSp9eSXo/ED+8Q7FAr+g=="
+    },
+    "node_modules/http-proxy-agent": {
+      "version": "5.0.0",
+      "resolved": 
"https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz";,
+      "integrity": 
"sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+      "dependencies": {
+        "@tootallnate/once": "2",
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "5.0.1",
+      "resolved": 
"https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz";,
+      "integrity": 
"sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+      "dependencies": {
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/ieee754": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz";,
+      "integrity": 
"sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross";
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross";
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support";
+        }
+      ]
+    },
+    "node_modules/import-fresh": {
+      "version": "3.3.0",
+      "resolved": 
"https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz";,
+      "integrity": 
"sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+      "dependencies": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus";
+      }
+    },
+    "node_modules/inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz";,
+      "integrity": 
"sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "dev": true,
+      "dependencies": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz";,
+      "integrity": 
"sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+    },
+    "node_modules/ini": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz";,
+      "integrity": 
"sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
+    },
+    "node_modules/is-arrayish": {
+      "version": "0.2.1",
+      "resolved": 
"https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz";,
+      "integrity": 
"sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": 
"https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz";,
+      "integrity": 
"sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/isarray": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz";,
+      "integrity": 
"sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
+      "dev": true
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz";,
+      "integrity": 
"sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz";,
+      "integrity": 
"sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+    },
+    "node_modules/js-yaml": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz";,
+      "integrity": 
"sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+      "dependencies": {
+        "argparse": "^2.0.1"
+      },
+      "bin": {
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
+    "node_modules/json-parse-even-better-errors": {
+      "version": "2.3.1",
+      "resolved": 
"https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz";,
+      "integrity": 
"sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
+    },
+    "node_modules/kdbush": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz";,
+      "integrity": 
"sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew=="
+    },
+    "node_modules/kind-of": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz";,
+      "integrity": 
"sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/lines-and-columns": {
+      "version": "1.2.4",
+      "resolved": 
"https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz";,
+      "integrity": 
"sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
+    },
+    "node_modules/lru-cache": {
+      "version": "4.1.5",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz";,
+      "integrity": 
"sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
+      "dependencies": {
+        "pseudomap": "^1.0.2",
+        "yallist": "^2.1.2"
+      }
+    },
+    "node_modules/maplibre-gl": {
+      "version": "2.4.0",
+      "resolved": 
"https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-2.4.0.tgz";,
+      "integrity": 
"sha512-csNFylzntPmHWidczfgCZpvbTSmhaWvLRj9e1ezUDBEPizGgshgm3ea1T5TCNEEBq0roauu7BPuRZjA3wO4KqA==",
+      "hasInstallScript": true,
+      "dependencies": {
+        "@mapbox/geojson-rewind": "^0.5.2",
+        "@mapbox/jsonlint-lines-primitives": "^2.0.2",
+        "@mapbox/mapbox-gl-supported": "^2.0.1",
+        "@mapbox/point-geometry": "^0.1.0",
+        "@mapbox/tiny-sdf": "^2.0.5",
+        "@mapbox/unitbezier": "^0.0.1",
+        "@mapbox/vector-tile": "^1.3.1",
+        "@mapbox/whoots-js": "^3.1.0",
+        "@types/geojson": "^7946.0.10",
+        "@types/mapbox__point-geometry": "^0.1.2",
+        "@types/mapbox__vector-tile": "^1.3.0",
+        "@types/pbf": "^3.0.2",
+        "csscolorparser": "~1.0.3",
+        "earcut": "^2.2.4",
+        "geojson-vt": "^3.2.1",
+        "gl-matrix": "^3.4.3",
+        "global-prefix": "^3.0.0",
+        "murmurhash-js": "^1.0.0",
+        "pbf": "^3.2.1",
+        "potpack": "^1.0.2",
+        "quickselect": "^2.0.0",
+        "supercluster": "^7.1.5",
+        "tinyqueue": "^2.0.3",
+        "vt-pbf": "^3.1.3"
+      }
+    },
+    "node_modules/mime": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz";,
+      "integrity": 
"sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz";,
+      "integrity": 
"sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz";,
+      "integrity": 
"sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb";
+      }
+    },
+    "node_modules/mitt": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.0.tgz";,
+      "integrity": 
"sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ=="
+    },
+    "node_modules/mkdirp": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz";,
+      "integrity": 
"sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+      "dev": true,
+      "bin": {
+        "mkdirp": "bin/cmd.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/mkdirp-classic": {
+      "version": "0.5.3",
+      "resolved": 
"https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz";,
+      "integrity": 
"sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
+    },
+    "node_modules/ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz";,
+      "integrity": 
"sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+    },
+    "node_modules/murmurhash-js": {
+      "version": "1.0.0",
+      "resolved": 
"https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz";,
+      "integrity": 
"sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw=="
+    },
+    "node_modules/negotiator": {
+      "version": "0.6.3",
+      "resolved": 
"https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz";,
+      "integrity": 
"sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/node-fetch": {
+      "version": "2.6.7",
+      "resolved": 
"https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz";,
+      "integrity": 
"sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
+      "dependencies": {
+        "whatwg-url": "^5.0.0"
+      },
+      "engines": {
+        "node": "4.x || >=6.0.0"
+      },
+      "peerDependencies": {
+        "encoding": "^0.1.0"
+      },
+      "peerDependenciesMeta": {
+        "encoding": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/noms": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz";,
+      "integrity": 
"sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==",
+      "dev": true,
+      "dependencies": {
+        "inherits": "^2.0.1",
+        "readable-stream": "~1.0.31"
+      }
+    },
+    "node_modules/noms/node_modules/readable-stream": {
+      "version": "1.0.34",
+      "resolved": 
"https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz";,
+      "integrity": 
"sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==",
+      "dev": true,
+      "dependencies": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.1",
+        "isarray": "0.0.1",
+        "string_decoder": "~0.10.x"
+      }
+    },
+    "node_modules/noms/node_modules/string_decoder": {
+      "version": "0.10.31",
+      "resolved": 
"https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz";,
+      "integrity": 
"sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==",
+      "dev": true
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz";,
+      "integrity": 
"sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/parent-module": {
+      "version": "1.0.1",
+      "resolved": 
"https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz";,
+      "integrity": 
"sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "dependencies": {
+        "callsites": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/parse-json": {
+      "version": "5.2.0",
+      "resolved": 
"https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz";,
+      "integrity": 
"sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+      "dependencies": {
+        "@babel/code-frame": "^7.0.0",
+        "error-ex": "^1.3.1",
+        "json-parse-even-better-errors": "^2.3.0",
+        "lines-and-columns": "^1.1.6"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus";
+      }
+    },
+    "node_modules/path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": 
"https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz";,
+      "integrity": 
"sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/path-type": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz";,
+      "integrity": 
"sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/pbf": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz";,
+      "integrity": 
"sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==",
+      "dependencies": {
+        "ieee754": "^1.1.12",
+        "resolve-protobuf-schema": "^2.1.0"
+      },
+      "bin": {
+        "pbf": "bin/pbf"
+      }
+    },
+    "node_modules/pend": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz";,
+      "integrity": 
"sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="
+    },
+    "node_modules/pixelmatch": {
+      "version": "5.3.0",
+      "resolved": 
"https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz";,
+      "integrity": 
"sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==",
+      "dependencies": {
+        "pngjs": "^6.0.0"
+      },
+      "bin": {
+        "pixelmatch": "bin/pixelmatch"
+      }
+    },
+    "node_modules/pixelmatch/node_modules/pngjs": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz";,
+      "integrity": 
"sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==",
+      "engines": {
+        "node": ">=12.13.0"
+      }
+    },
+    "node_modules/pngjs": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz";,
+      "integrity": 
"sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
+      "engines": {
+        "node": ">=14.19.0"
+      }
+    },
+    "node_modules/potpack": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz";,
+      "integrity": 
"sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ=="
+    },
+    "node_modules/prettier": {
+      "version": "2.8.8",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz";,
+      "integrity": 
"sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+      "dev": true,
+      "bin": {
+        "prettier": "bin-prettier.js"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1";
+      }
+    },
+    "node_modules/process-nextick-args": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz";,
+      "integrity": 
"sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+      "dev": true
+    },
+    "node_modules/progress": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz";,
+      "integrity": 
"sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/protocol-buffers-schema": {
+      "version": "3.6.0",
+      "resolved": 
"https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz";,
+      "integrity": 
"sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="
+    },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": 
"https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz";,
+      "integrity": 
"sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+    },
+    "node_modules/pseudomap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz";,
+      "integrity": 
"sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ=="
+    },
+    "node_modules/pump": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz";,
+      "integrity": 
"sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+      "dependencies": {
+        "end-of-stream": "^1.1.0",
+        "once": "^1.3.1"
+      }
+    },
+    "node_modules/puppeteer": {
+      "version": "20.2.0",
+      "resolved": 
"https://registry.npmjs.org/puppeteer/-/puppeteer-20.2.0.tgz";,
+      "integrity": 
"sha512-mGiCtWGq7S8MRUefZQV0AUiYGSPGmVlL/eYKPB4X9s9x0kuhzJmbSXSoWYFyo8MXaFmjUOqeMNP/3KeLZw207A==",
+      "hasInstallScript": true,
+      "dependencies": {
+        "@puppeteer/browsers": "1.2.0",
+        "cosmiconfig": "8.1.3",
+        "puppeteer-core": "20.2.0"
+      }
+    },
+    "node_modules/puppeteer-core": {
+      "version": "20.2.0",
+      "resolved": 
"https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-20.2.0.tgz";,
+      "integrity": 
"sha512-QKLu4ELW/609vlLqp/NvQA564LzvLaF6x1ZH+4BhPhxPgreoY8WR/AYOzXS12hgtPPjNCqPKiRLxmyXGnlIK8A==",
+      "dependencies": {
+        "@puppeteer/browsers": "1.2.0",
+        "chromium-bidi": "0.4.7",
+        "cross-fetch": "3.1.5",
+        "debug": "4.3.4",
+        "devtools-protocol": "0.0.1120988",
+        "ws": "8.13.0"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      },
+      "peerDependencies": {
+        "typescript": ">= 4.7.4"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/quickselect": {
+      "version": "2.0.0",
+      "resolved": 
"https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz";,
+      "integrity": 
"sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw=="
+    },
+    "node_modules/readable-stream": {
+      "version": "3.6.2",
+      "resolved": 
"https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz";,
+      "integrity": 
"sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/require-directory": {
+      "version": "2.1.1",
+      "resolved": 
"https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz";,
+      "integrity": 
"sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/resolve-from": {
+      "version": "4.0.0",
+      "resolved": 
"https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz";,
+      "integrity": 
"sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/resolve-protobuf-schema": {
+      "version": "2.1.0",
+      "resolved": 
"https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz";,
+      "integrity": 
"sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
+      "dependencies": {
+        "protocol-buffers-schema": "^3.3.1"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": 
"https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz";,
+      "integrity": 
"sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross";
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross";
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support";
+        }
+      ]
+    },
+    "node_modules/st": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/st/-/st-3.0.0.tgz";,
+      "integrity": 
"sha512-UEUi8P8Y5GOewlJbE5vrhsaQRwmbNVMUr6PLxRZHH4Cwz8CkHhnBqlqGtE3egXQd+ceUwNxdOVjsC/IsgN2Pww==",
+      "dependencies": {
+        "async-cache": "^1.1.0",
+        "bl": "^5.0.0",
+        "fd": "~0.0.3",
+        "mime": "^2.5.2",
+        "negotiator": "~0.6.2"
+      },
+      "bin": {
+        "st": "bin/server.js"
+      },
+      "optionalDependencies": {
+        "graceful-fs": "^4.2.3"
+      }
+    },
+    "node_modules/st/node_modules/bl": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz";,
+      "integrity": 
"sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==",
+      "dependencies": {
+        "buffer": "^6.0.3",
+        "inherits": "^2.0.4",
+        "readable-stream": "^3.4.0"
+      }
+    },
+    "node_modules/st/node_modules/buffer": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz";,
+      "integrity": 
"sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross";
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross";
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support";
+        }
+      ],
+      "dependencies": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.2.1"
+      }
+    },
+    "node_modules/string_decoder": {
+      "version": "1.3.0",
+      "resolved": 
"https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz";,
+      "integrity": 
"sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+      "dependencies": {
+        "safe-buffer": "~5.2.0"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": 
"https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz";,
+      "integrity": 
"sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": 
"https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz";,
+      "integrity": 
"sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/supercluster": {
+      "version": "7.1.5",
+      "resolved": 
"https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz";,
+      "integrity": 
"sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==",
+      "dependencies": {
+        "kdbush": "^3.0.0"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "5.5.0",
+      "resolved": 
"https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz";,
+      "integrity": 
"sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+      "dependencies": {
+        "has-flag": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/tar-fs": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz";,
+      "integrity": 
"sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
+      "dependencies": {
+        "chownr": "^1.1.1",
+        "mkdirp-classic": "^0.5.2",
+        "pump": "^3.0.0",
+        "tar-stream": "^2.1.4"
+      }
+    },
+    "node_modules/tar-stream": {
+      "version": "2.2.0",
+      "resolved": 
"https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz";,
+      "integrity": 
"sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+      "dependencies": {
+        "bl": "^4.0.3",
+        "end-of-stream": "^1.4.1",
+        "fs-constants": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^3.1.1"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/through": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz";,
+      "integrity": 
"sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
+    },
+    "node_modules/through2": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz";,
+      "integrity": 
"sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+      "dev": true,
+      "dependencies": {
+        "readable-stream": "~2.3.6",
+        "xtend": "~4.0.1"
+      }
+    },
+    "node_modules/through2/node_modules/isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz";,
+      "integrity": 
"sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+      "dev": true
+    },
+    "node_modules/through2/node_modules/readable-stream": {
+      "version": "2.3.8",
+      "resolved": 
"https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz";,
+      "integrity": 
"sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+      "dev": true,
+      "dependencies": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.3",
+        "isarray": "~1.0.0",
+        "process-nextick-args": "~2.0.0",
+        "safe-buffer": "~5.1.1",
+        "string_decoder": "~1.1.1",
+        "util-deprecate": "~1.0.1"
+      }
+    },
+    "node_modules/through2/node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": 
"https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz";,
+      "integrity": 
"sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+      "dev": true
+    },
+    "node_modules/through2/node_modules/string_decoder": {
+      "version": "1.1.1",
+      "resolved": 
"https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz";,
+      "integrity": 
"sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+      "dev": true,
+      "dependencies": {
+        "safe-buffer": "~5.1.0"
+      }
+    },
+    "node_modules/tinyqueue": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz";,
+      "integrity": 
"sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA=="
+    },
+    "node_modules/tr46": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz";,
+      "integrity": 
"sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+    },
+    "node_modules/typescript": {
+      "version": "5.0.4",
+      "resolved": 
"https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz";,
+      "integrity": 
"sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==",
+      "devOptional": true,
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=12.20"
+      }
+    },
+    "node_modules/unbzip2-stream": {
+      "version": "1.4.3",
+      "resolved": 
"https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz";,
+      "integrity": 
"sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==",
+      "dependencies": {
+        "buffer": "^5.2.1",
+        "through": "^2.3.8"
+      }
+    },
+    "node_modules/untildify": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz";,
+      "integrity": 
"sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": 
"https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz";,
+      "integrity": 
"sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+    },
+    "node_modules/vt-pbf": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz";,
+      "integrity": 
"sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==",
+      "dependencies": {
+        "@mapbox/point-geometry": "0.1.0",
+        "@mapbox/vector-tile": "^1.3.1",
+        "pbf": "^3.2.1"
+      }
+    },
+    "node_modules/webidl-conversions": {
+      "version": "3.0.1",
+      "resolved": 
"https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz";,
+      "integrity": 
"sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+    },
+    "node_modules/whatwg-url": {
+      "version": "5.0.0",
+      "resolved": 
"https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz";,
+      "integrity": 
"sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+      "dependencies": {
+        "tr46": "~0.0.3",
+        "webidl-conversions": "^3.0.0"
+      }
+    },
+    "node_modules/which": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz";,
+      "integrity": 
"sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "which": "bin/which"
+      }
+    },
+    "node_modules/wrap-ansi": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz";,
+      "integrity": 
"sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1";
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": 
"https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz";,
+      "integrity": 
"sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1";
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": 
"https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz";,
+      "integrity": 
"sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": 
"https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz";,
+      "integrity": 
"sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz";,
+      "integrity": 
"sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+    },
+    "node_modules/ws": {
+      "version": "8.13.0",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz";,
+      "integrity": 
"sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/xtend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz";,
+      "integrity": 
"sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.4"
+      }
+    },
+    "node_modules/y18n": {
+      "version": "5.0.8",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz";,
+      "integrity": 
"sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/yallist": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz";,
+      "integrity": 
"sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A=="
+    },
+    "node_modules/yargs": {
+      "version": "17.7.1",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz";,
+      "integrity": 
"sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==",
+      "dependencies": {
+        "cliui": "^8.0.1",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.3",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^21.1.1"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/yargs-parser": {
+      "version": "21.1.1",
+      "resolved": 
"https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz";,
+      "integrity": 
"sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/yauzl": {
+      "version": "2.10.0",
+      "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz";,
+      "integrity": 
"sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
+      "dependencies": {
+        "buffer-crc32": "~0.2.3",
+        "fd-slicer": "~1.1.0"
+      }
+    }
+  }
+}
diff --git a/baremaps-renderer/package.json b/baremaps-renderer/package.json
new file mode 100644
index 00000000..49844dbe
--- /dev/null
+++ b/baremaps-renderer/package.json
@@ -0,0 +1,34 @@
+{
+  "name": "baremaps-renderer",
+  "version": "0.0.2",
+  "description": "Integration tests for pixel to pixel matching",
+  "bin": {
+    "baremaps-renderer": "./bin/index.js"
+  },
+  "scripts": {
+    "format": "prettier --write .",
+    "build": "tsc && copyfiles ./assets/**/* ./bin && npm run chmod",
+    "chmod": "chmod u+x ./bin/index.js"
+  },
+  "author": "Leonard Cseres <[email protected]>",
+  "license": "Apache-2.0",
+  "dependencies": {
+    "chalk": "^4",
+    "commander": "^10.0.1",
+    "http": "^0.0.1-security",
+    "maplibre-gl": "^2.4.0",
+    "pixelmatch": "^5.3.0",
+    "pngjs": "^7.0.0",
+    "puppeteer": "^20.2.0",
+    "st": "^3.0.0"
+  },
+  "devDependencies": {
+    "@types/node": "^20.1.4",
+    "@types/pixelmatch": "^5.2.4",
+    "@types/pngjs": "^6.0.1",
+    "@types/serve-handler": "^6.1.1",
+    "copyfiles": "^2.4.1",
+    "prettier": "^2.8.8",
+    "typescript": "^5.0.4"
+  }
+}
diff --git a/baremaps-renderer/src/index.ts b/baremaps-renderer/src/index.ts
new file mode 100644
index 00000000..59f1c43c
--- /dev/null
+++ b/baremaps-renderer/src/index.ts
@@ -0,0 +1,129 @@
+#!/usr/bin/env node
+
+/**
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not 
use this file except
+ in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software 
distributed under the License
+ is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
KIND, either express
+ or implied. See the License for the specific language governing permissions 
and limitations under
+ the License.
+ **/
+
+import commander from 'commander';
+import http from 'http';
+import st from 'st';
+import { exec } from 'child_process';
+import { BrowserPool } from './lib/browserPool';
+import { TestsLogger } from './lib/testsLogger';
+import { MaplibreBrowserHelpers } from './lib/maplibreBrowserHelpers';
+import { TestManager } from './lib/testManager';
+import { ReportGenerator } from './lib/reportGenerator';
+
+/** Subcommand for running the tests */
+const run = async (options: any) => {
+  // Parse options
+  const instances = parseInt(options.instances);
+  const threshold = parseFloat(options.threshold);
+
+  // Discover tests
+  const testLogger = new TestsLogger();
+  const testManager = new TestManager(
+    options.path,
+    'integration',
+    options.style,
+    options.refStyle,
+    testLogger,
+    threshold,
+  );
+
+  // Setup browser pool
+  const maplibreHelpers = new MaplibreBrowserHelpers(options.debug);
+  const browserPool = new BrowserPool(
+    instances,
+    true,
+    maplibreHelpers.initPage,
+  );
+  await browserPool.init();
+
+  // Queue tests
+  for (const test of testManager.getTests()) {
+    browserPool.queueTask(test);
+  }
+
+  // Run tests
+  const testResults: boolean[] = await browserPool.run();
+  await browserPool.close();
+
+  // Generate summary and report
+  testLogger.logSummary();
+  new ReportGenerator(options.path, testManager.getTests()).generate();
+
+  if (testResults.some((res) => !res)) {
+    process.exit(1);
+  }
+};
+
+/** Subcommand for viewing the report */
+const viewReport = async (options: any) => {
+  const server = http.createServer(
+    st({ path: options.path, index: ReportGenerator.reportFilename }),
+  );
+
+  const port = 3000;
+  const url = `http://localhost:${port}`;
+  server.listen(port, () => {
+    console.log(`INFO: Running at ${url}`);
+  });
+  // open the browser
+  if (options.open) {
+    if (process.platform === 'linux') {
+      exec(`xdg-open ${url}`);
+    } else if (process.platform === 'darwin') {
+      exec(`open ${url}`);
+    } else if (process.platform === 'win32') {
+      exec(`start ${url}`);
+    }
+  }
+};
+
+/** Main entrypoint */
+(async () => {
+  const program = new commander.Command();
+
+  program.version('0.0.2').description('Baremaps renderer utility tool');
+
+  program
+    .command('run')
+    .description('run integration tests defined in the tests folder')
+    .requiredOption('-s, --style <style>', 'style url to use')
+    .option(
+      '-r, --refStyle <refStyle>',
+      'reference style url to use',
+      'https://demo.baremaps.com/style.json',
+    )
+    .option(
+      '-t, --threshold <threshold>',
+      'threshold to use for comparing images',
+      '0.1',
+    )
+    .option('-p, --path <testsPath>', 'tests folder path to use', 'tests')
+    .option('-d, --debug', 'debug output', false)
+    .option(
+      '-i, --instances <instances>',
+      'number of concurrent browsers to use',
+      '2',
+    )
+    .action(run);
+
+  program
+    .command('report')
+    .description('view the report of the latest test run')
+    .option('-o, --open', 'open the report in the browser', false)
+    .option('-p, --path <testsPath>', 'tests folder path to use', 'tests')
+    .action(viewReport);
+
+  program.parse(process.argv);
+})();
diff --git a/baremaps-renderer/src/lib/browserPool.ts 
b/baremaps-renderer/src/lib/browserPool.ts
new file mode 100644
index 00000000..9f094bb3
--- /dev/null
+++ b/baremaps-renderer/src/lib/browserPool.ts
@@ -0,0 +1,86 @@
+/**
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not 
use this file except
+ in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software 
distributed under the License
+ is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
KIND, either express
+ or implied. See the License for the specific language governing permissions 
and limitations under
+ the License.
+ **/
+
+import puppeteer, { Page, Browser } from 'puppeteer';
+import { RunnableTask } from './runnableTask';
+
+/** Pool of browsers that can be used to run tasks in parallel */
+export class BrowserPool {
+  private instances: number;
+  private headless: boolean;
+  private initPage: (page: Page) => Promise<any>;
+  private pool: Browser[];
+  private queue: RunnableTask[] = [];
+
+  /**
+   * Create a new BrowserPool
+   *
+   * @param instances number of browsers to create
+   * @param headless whether to run the browsers in headless mode
+   * @param initPage function to run on each page before running a task
+   * @returns a new BrowserPool
+   */
+  constructor(
+    instances: number,
+    headless: boolean = true,
+    initPage?: (page: Page) => Promise<any>,
+  ) {
+    this.instances = instances;
+    this.headless = headless;
+    this.pool = [];
+    this.initPage = initPage || (async (page: Page) => {});
+  }
+
+  /** Initialize the browser pool */
+  public async init() {
+    for (let i = 0; i < this.instances; i++) {
+      const browser = await puppeteer.launch({
+        headless: this.headless ? 'new' : false,
+        args: ['--enable-webgl', '--no-sandbox', '--disable-web-security'],
+      });
+      const [page] = await browser.pages();
+      await this.initPage(page);
+      this.pool.push(browser);
+    }
+  }
+
+  /**
+   * Queue a task to be run
+   *
+   * @param task task to run
+   */
+  public async queueTask(task: RunnableTask) {
+    this.queue.push(task);
+  }
+
+  /** Run all queued tasks in parallel */
+  public async run() {
+    const results = [];
+    // run the queue in parallel with only one browser per task at max
+    while (this.queue.length) {
+      const res = await Promise.all(
+        this.queue.splice(0, this.instances).map(async (task, index) => {
+          const browser = this.pool[index];
+          const [page] = await browser.pages();
+          return await task.run(page);
+        }),
+      );
+      results.push(...res);
+    }
+    return results;
+  }
+
+  /** Close all browsers in the pool */
+  public async close() {
+    await Promise.all(this.pool.map((browser) => browser.close()));
+  }
+}
diff --git a/baremaps-renderer/src/lib/maplibreBrowserHelpers.ts 
b/baremaps-renderer/src/lib/maplibreBrowserHelpers.ts
new file mode 100644
index 00000000..8407cc66
--- /dev/null
+++ b/baremaps-renderer/src/lib/maplibreBrowserHelpers.ts
@@ -0,0 +1,128 @@
+/**
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not 
use this file except
+ in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software 
distributed under the License
+ is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
KIND, either express
+ or implied. See the License for the specific language governing permissions 
and limitations under
+ the License.
+ **/
+
+import { Page } from 'puppeteer';
+import { Metadata } from '../types';
+
+/** Helper class for running maplibre-gl in a browser */
+export class MaplibreBrowserHelpers {
+  private debug: boolean;
+
+  /**
+   * Create a new MaplibreBrowserHelpers
+   *
+   * @param debug whether to enable debug logging
+   */
+  constructor(debug: boolean = false) {
+    this.debug = debug;
+  }
+
+  /**
+   * Initialize a page for running maplibre-gl
+   *
+   * @param page puppeteer page
+   */
+  public async initPage(page: Page) {
+    if (this.debug) {
+      page
+        .on('console', (message: any) =>
+          console.log(
+            `${message.type().slice(0, 3).toUpperCase()} ${message.text()}`,
+          ),
+        )
+        .on('pageerror', ({ message }: any) => console.log(message))
+        .on('response', (response: any) =>
+          console.log(`${response.status()} ${response.url()}`),
+        )
+        .on('requestfailed', (request: any) =>
+          console.log(`${request.failure().errorText} ${request.url()}`),
+        );
+    }
+
+    await page.addScriptTag({
+      url: 'https://unpkg.com/[email protected]/dist/maplibre-gl.js',
+    });
+    await page.addStyleTag({
+      url: 'https://unpkg.com/[email protected]/dist/maplibre-gl.css',
+    });
+  }
+
+  /**
+   * Get an image from a maplibre-gl style
+   *
+   * @param page puppeteer page
+   * @param metadata metadata for the map
+   * @param styleUrl url to the maplibre-gl style
+   * @returns image as a base64 encoded string
+   */
+  public static async getImageFromMetadata(
+    page: Page,
+    metadata: Metadata,
+    styleUrl: string,
+  ) {
+    const width = metadata.width;
+    const height = metadata.height;
+
+    await page.setViewport({ width, height, deviceScaleFactor: 1 });
+    await page.setContent(`
+        <!DOCTYPE html>
+        <html lang="en">
+        <head>
+            <title>Query Test Page</title>
+            <meta charset='utf-8'>
+            <link rel="icon" href="about:blank">
+            <style>#map {
+                box-sizing:content-box;
+                width:${width}px;
+                height:${height}px;
+            }</style>
+        </head>
+        <body>
+            <div id='map'></div>
+        </body>
+        </html>`);
+
+    // create map
+    await page.evaluate(
+      (metadata: Metadata, styleUrl: string) => {
+        (window as any).map = new (window as any).maplibregl.Map({
+          container: 'map',
+          style: styleUrl,
+          center: metadata.center,
+          zoom: metadata.zoom,
+          interactive: false,
+          attributionControl: false,
+          // If true, the map's canvas can be exported to a PNG using 
map.getCanvas().toDataURL(). This is false by default as a performance 
optimization.
+          preserveDrawingBuffer: true,
+          // Prevents the fading-in of layers after the style has loaded.
+          fadeDuration: 0,
+        });
+      },
+      metadata,
+      styleUrl,
+    );
+
+    // wait for map to load
+    await page.waitForFunction(() => {
+      return (
+        (window as any).map?.loaded() && (window as any).map?.isStyleLoaded()
+      );
+    });
+
+    // export image from map
+    const image = await page.evaluate(() => {
+      return (window as any).map.getCanvas().toDataURL();
+    });
+
+    return Buffer.from(image.split(',')[1], 'base64');
+  }
+}
diff --git a/baremaps-renderer/src/lib/reportGenerator.ts 
b/baremaps-renderer/src/lib/reportGenerator.ts
new file mode 100644
index 00000000..a30aa873
--- /dev/null
+++ b/baremaps-renderer/src/lib/reportGenerator.ts
@@ -0,0 +1,124 @@
+/**
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not 
use this file except
+ in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software 
distributed under the License
+ is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
KIND, either express
+ or implied. See the License for the specific language governing permissions 
and limitations under
+ the License.
+ **/
+
+import fs from 'fs';
+import path from 'path';
+import { Test } from './test';
+import { TestData } from '../types';
+
+/**
+ * ReportGenerator class
+ *
+ * Generates an HTML report from generated images during tests
+ */
+export class ReportGenerator {
+  public static readonly reportFilename = 'report.html';
+  private static readonly assetsPath = 'assets';
+  private static readonly reportTemplateFilename = 'report-template.html';
+
+  private testFolder: string;
+  private tests: Test[];
+  private htmlTemplate: string;
+
+  /**
+   * Creates a new ReportGenerator
+   *
+   * @param testFolder Path to the test folder
+   * @param tests Array of tests
+   * @returns A new ReportGenerator
+   */
+  constructor(testFolder: string, tests: Test[]) {
+    this.testFolder = testFolder;
+    if (!path.isAbsolute(this.testFolder)) {
+      this.testFolder = path.join(process.cwd(), this.testFolder);
+    }
+    this.tests = tests;
+    // root dir of the package
+    const rootDirname = path.dirname(__dirname);
+    this.htmlTemplate = fs.readFileSync(
+      path.join(
+        rootDirname,
+        ReportGenerator.assetsPath,
+        ReportGenerator.reportTemplateFilename,
+      ),
+      'utf8',
+    );
+  }
+
+  /** Generates and saves the HTML report */
+  public generate() {
+    const testsData = this.tests
+      .map((test) => this.getTestData(test))
+      .filter((test) => test !== null);
+
+    const html = this.htmlTemplate.replace(
+      '{{ TESTS_DATA }}',
+      JSON.stringify(testsData, null, 2),
+    );
+    this.writeHtml(html);
+
+    console.log(
+      "INFO: Report generated at '" +
+        path.join(this.testFolder, ReportGenerator.reportFilename) +
+        "'",
+    );
+  }
+
+  /**
+   * Returns an object with the test images paths and metadata for a given test
+   *
+   * @param test The test to get the data from
+   * @returns An object with the test images paths and metadata
+   */
+  private getTestData(test: Test): TestData | null {
+    // return if the test is not completed
+    if (test.success === undefined) {
+      return null;
+    }
+    // check if the images exist
+    if (
+      !fs.existsSync(path.join(test.testPath, Test.expectedFilename)) ||
+      !fs.existsSync(path.join(test.testPath, Test.actualFilename)) ||
+      !fs.existsSync(path.join(test.testPath, Test.diffFilename))
+    ) {
+      console.log(
+        'WARN: Missing images for test ' + test.testPath + ', skipping...',
+      );
+      return null;
+    }
+    const testName = path.basename(test.testPath);
+    const testsRelativePath = path.relative(this.testFolder, test.testPath);
+    return {
+      path: test.testPath,
+      name: testName,
+      metadata: test.metadata,
+      success: test.success,
+      expectedImagePath: path.join(testsRelativePath, Test.expectedFilename),
+      actualImagePath: path.join(testsRelativePath, Test.actualFilename),
+      diffImagePath: path.join(testsRelativePath, Test.diffFilename),
+      // The diff is not undefined because the test is completed
+      diff: test.diff as number,
+    };
+  }
+
+  /**
+   * Writes the HTML report to disk
+   *
+   * @param html The HTML report
+   */
+  private writeHtml(html: string) {
+    fs.writeFileSync(
+      path.join(this.testFolder, ReportGenerator.reportFilename),
+      html,
+    );
+  }
+}
diff --git a/baremaps-renderer/src/lib/runnableTask.ts 
b/baremaps-renderer/src/lib/runnableTask.ts
new file mode 100644
index 00000000..e860f216
--- /dev/null
+++ b/baremaps-renderer/src/lib/runnableTask.ts
@@ -0,0 +1,24 @@
+/**
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not 
use this file except
+ in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software 
distributed under the License
+ is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
KIND, either express
+ or implied. See the License for the specific language governing permissions 
and limitations under
+ the License.
+ **/
+
+import { Page } from 'puppeteer';
+
+/** Abstract class for a task that can be run on a puppeteer page */
+export abstract class RunnableTask {
+  /**
+   * Abstract function for running the task
+   *
+   * @param page puppeteer page
+   * @returns true if the task was successful, false otherwise
+   */
+  abstract run(page: Page): Promise<boolean>;
+}
diff --git a/baremaps-renderer/src/lib/test.ts 
b/baremaps-renderer/src/lib/test.ts
new file mode 100644
index 00000000..340226a3
--- /dev/null
+++ b/baremaps-renderer/src/lib/test.ts
@@ -0,0 +1,178 @@
+/**
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not 
use this file except
+ in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software 
distributed under the License
+ is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
KIND, either express
+ or implied. See the License for the specific language governing permissions 
and limitations under
+ the License.
+ **/
+
+import fs from 'fs';
+import path from 'path';
+import pixelmatch from 'pixelmatch';
+import { PNG } from 'pngjs';
+import { Page } from 'puppeteer';
+import { MaplibreBrowserHelpers } from './maplibreBrowserHelpers';
+import { TestsLogger } from './testsLogger';
+import { Metadata } from '../types';
+import { RunnableTask } from './runnableTask';
+
+/** Test is responsible for running a single test */
+export class Test implements RunnableTask {
+  public static readonly metadataFilename = 'metadata.json';
+  public static readonly expectedFilename = 'expected.png';
+  public static readonly actualFilename = 'actual.png';
+  public static readonly diffFilename = 'diff.png';
+
+  private _testPath: string;
+  private styleUrl: string;
+  private refStyleUrl: string;
+  private testLogger: TestsLogger;
+  private threshold: number;
+
+  private _metadata: Metadata;
+
+  private _success?: boolean;
+  private _diff?: number;
+
+  /**
+   * Create a new Test
+   *
+   * @param testPath path to the test
+   * @param styleUrl URL to the style
+   * @param refStyleUrl URL to the reference style
+   * @param testLogger logger for tests
+   * @param threshold threshold for image comparison
+   * @returns a new Test
+   * @throws if the test is not setup correctly
+   */
+  constructor(
+    testPath: string,
+    styleUrl: string,
+    refStyleUrl: string,
+    testLogger: TestsLogger,
+    threshold: number,
+  ) {
+    this._testPath = testPath;
+    this.styleUrl = styleUrl;
+    this.refStyleUrl = refStyleUrl;
+    this.testLogger = testLogger;
+    this.threshold = threshold;
+    try {
+      this._metadata = JSON.parse(
+        fs
+          .readFileSync(path.join(this.testPath, Test.metadataFilename))
+          .toString(),
+      );
+    } catch (e) {
+      throw new Error(
+        `Could not read metadata in test '${this.testPath}'. ` +
+          `Make sure the file '${Test.metadataFilename}' exists and is valid 
JSON`,
+      );
+    }
+    // check if metadata is valid
+    if (
+      !this.metadata ||
+      !this.metadata.width ||
+      !this.metadata.height ||
+      !this.metadata.center ||
+      !this.metadata.zoom
+    ) {
+      throw new Error(
+        `Invalid metadata in test '${this.testPath}'. ` +
+          'Metadata must contain width, height, center and zoom',
+      );
+    }
+  }
+
+  public get testPath(): string {
+    return this._testPath;
+  }
+
+  public get metadata(): Metadata {
+    return this._metadata;
+  }
+
+  public get success(): boolean | undefined {
+    return this._success;
+  }
+
+  public get diff(): number | undefined {
+    return this._diff;
+  }
+
+  /**
+   * Abstraction function for running the test
+   *
+   * @param page puppeteer page
+   * @returns true if the test was successful, false otherwise
+   * @throws if the test fails to run
+   */
+  public async run(page: Page): Promise<boolean> {
+    const success = await this.runTest(page)
+      .then((s: boolean) => {
+        this.testLogger.logTest(this.testPath, s);
+        return s;
+      })
+      .catch((e) => {
+        this.testLogger.logError(this.testPath, e);
+        return false;
+      });
+    this._success = success;
+    return success;
+  }
+
+  /**
+   * Run the test
+   *
+   * @param page puppeteer page
+   * @returns true if the test was successful, false otherwise
+   */
+  private async runTest(page: Page): Promise<boolean> {
+    const image = await MaplibreBrowserHelpers.getImageFromMetadata(
+      page,
+      this.metadata,
+      this.styleUrl,
+    );
+
+    const refImage = await MaplibreBrowserHelpers.getImageFromMetadata(
+      page,
+      this.metadata,
+      this.refStyleUrl,
+    );
+
+    // compare image to reference image
+    const width = this.metadata.width;
+    const height = this.metadata.height;
+
+    const diffImg = new PNG({ width, height });
+
+    const diff = pixelmatch(
+      PNG.sync.read(refImage).data,
+      PNG.sync.read(image).data,
+      diffImg.data,
+      width,
+      height,
+      { threshold: this.threshold / (width * height) },
+    );
+    this._diff = diff;
+
+    // save actual image
+    fs.writeFileSync(path.join(this.testPath, Test.actualFilename), image);
+    // save expected image
+    fs.writeFileSync(path.join(this.testPath, Test.expectedFilename), 
refImage);
+    // save diff image
+    fs.writeFileSync(
+      path.join(this.testPath, Test.diffFilename),
+      PNG.sync.write(diffImg, { filterType: 4 }),
+    );
+
+    if (diff > 0) {
+      return false;
+    }
+    return true;
+  }
+}
diff --git a/baremaps-renderer/src/lib/testManager.ts 
b/baremaps-renderer/src/lib/testManager.ts
new file mode 100644
index 00000000..e2a65af2
--- /dev/null
+++ b/baremaps-renderer/src/lib/testManager.ts
@@ -0,0 +1,90 @@
+/**
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not 
use this file except
+ in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software 
distributed under the License
+ is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
KIND, either express
+ or implied. See the License for the specific language governing permissions 
and limitations under
+ the License.
+ **/
+
+import fs from 'fs';
+import path from 'path';
+import { Test } from './test';
+import { TestsLogger } from './testsLogger';
+
+/** TestManager is responsible for discovering and creating tests */
+export class TestManager {
+  private testsPath: string;
+  private tests: Test[];
+
+  /**
+   * Create a new TestManager
+   *
+   * @param testsFolder path to the tests folder
+   * @param integrationFolder path to the integration folder
+   * @param styleUrl URL to the style
+   * @param refStyleUrl URL to the reference style
+   * @param testLogger logger for tests
+   * @param threshold threshold for image comparison
+   * @returns a new TestManager
+   * @throws if the test is not setup correctly
+   */
+  constructor(
+    testsFolder: string,
+    integrationFolder: string,
+    styleUrl: string,
+    refStyleUrl: string,
+    testLogger: TestsLogger,
+    threshold: number,
+  ) {
+    this.testsPath = path.join(testsFolder, integrationFolder);
+    // check if this.testsPath is absolute
+    if (!path.isAbsolute(this.testsPath)) {
+      this.testsPath = path.join(process.cwd(), this.testsPath);
+    }
+    this.tests = [] as Test[];
+    const testNames = this.discoverTests();
+    for (const testName of testNames) {
+      try {
+        this.tests.push(
+          new Test(
+            path.join(this.testsPath, testName),
+            styleUrl,
+            refStyleUrl,
+            testLogger,
+            threshold,
+          ),
+        );
+      } catch (e) {
+        testLogger.logError(testName, e);
+      }
+    }
+  }
+
+  public getTests(): Test[] {
+    return this.tests;
+  }
+
+  /**
+   * Discover tests in the tests folder
+   * @returns a list of test names
+   * @throws if the tests folder is not found
+   */
+  public discoverTests() {
+    let testNames: string[];
+    try {
+      // filter by directories
+      testNames = fs
+        .readdirSync(this.testsPath)
+        .filter((file) =>
+          fs.statSync(path.join(this.testsPath, file)).isDirectory(),
+        );
+    } catch (e) {
+      throw new Error(`Tests folder '${this.testsPath}' could not be found`);
+    }
+    return testNames;
+  }
+}
diff --git a/baremaps-renderer/src/lib/testsLogger.ts 
b/baremaps-renderer/src/lib/testsLogger.ts
new file mode 100644
index 00000000..3b66558a
--- /dev/null
+++ b/baremaps-renderer/src/lib/testsLogger.ts
@@ -0,0 +1,112 @@
+/**
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not 
use this file except
+ in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software 
distributed under the License
+ is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
KIND, either express
+ or implied. See the License for the specific language governing permissions 
and limitations under
+ the License.
+ **/
+
+import path from 'path';
+import chalk from 'chalk';
+import { TestResults } from '../types';
+
+/** Class to log tests */
+export class TestsLogger {
+  private tests: TestResults;
+  private startTime: number;
+
+  /**
+   * Create a new TestsLogger
+   *
+   * @returns a new TestsLogger
+   */
+  constructor() {
+    this.tests = {} as TestResults;
+    this.startTime = Date.now();
+  }
+
+  /**
+   * Log a test to the console
+   *
+   * @param testPath path to the test
+   * @param success whether the test was successful
+   */
+  public logTest(testPath: string, success: boolean) {
+    const testName = path.basename(testPath);
+    this.tests[testName] = success;
+
+    const testRepr = this.getTestRepr(testPath);
+    console.log(
+      success
+        ? chalk.bgGreenBright(chalk.whiteBright(chalk.bold(' PASS '))) +
+            testRepr
+        : chalk.bgRedBright(chalk.whiteBright(chalk.bold(' FAIL '))) + 
testRepr,
+    );
+  }
+
+  /**
+   * Log an error to the console
+   *
+   * @param testPath path to the test
+   * @param error error to log
+   */
+  public logError(testPath: string, error: Error | unknown) {
+    const testName = path.basename(testPath);
+    this.tests[testName] = false;
+
+    const testRepr = this.getTestRepr(testPath);
+    console.log(
+      chalk.bgRedBright(chalk.whiteBright(chalk.bold(' ERROR '))) + testRepr,
+    );
+    console.log(error);
+  }
+
+  /** Log a summary of the tests to the console */
+  public logSummary() {
+    // add empty line
+    console.log();
+    console.log(
+      chalk.bgWhiteBright(chalk.blackBright(chalk.bold(' SUMMARY '))),
+    );
+
+    // log time taken
+    const timeTaken = Date.now() - this.startTime;
+    console.log(
+      'Time taken: ' +
+        chalk.bold(`${Math.round(timeTaken / 1000 / 60)}m`) +
+        chalk.bold(`${(timeTaken / 1000).toFixed(2)}s`),
+    );
+
+    // print summary total failed and total passed
+    const totPassed = Object.values(this.tests).filter(
+      (success) => success,
+    ).length;
+    const totFailed = Object.values(this.tests).length - totPassed;
+
+    console.log(
+      'Out of ' +
+        chalk.bold(Object.values(this.tests).length) +
+        ' tests: ' +
+        chalk.greenBright(chalk.bold(`${totPassed} passed`)) +
+        ', ' +
+        chalk.redBright(chalk.bold(`${totFailed} failed `)),
+    );
+
+    // add empty line
+    console.log();
+  }
+
+  /**
+   * Helper function to get a string representation for a test
+   *
+   * @param testPath path to the test
+   * @returns string representation of the test
+   */
+  private getTestRepr(testPath: string) {
+    return ` Ran test: ${path.basename(testPath)} (${testPath})`;
+  }
+}
diff --git a/baremaps-renderer/src/types/index.ts 
b/baremaps-renderer/src/types/index.ts
new file mode 100644
index 00000000..99fe9e90
--- /dev/null
+++ b/baremaps-renderer/src/types/index.ts
@@ -0,0 +1,36 @@
+/**
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not 
use this file except
+ in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software 
distributed under the License
+ is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
KIND, either express
+ or implied. See the License for the specific language governing permissions 
and limitations under
+ the License.
+ **/
+
+/** Represent the result of tests */
+export interface TestResults {
+  [key: string]: boolean | undefined;
+}
+
+/** Metadata file spec for tests */
+export interface Metadata {
+  width: number;
+  height: number;
+  center: [number, number];
+  zoom: number;
+}
+
+/** Data spec for the HTML generator */
+export interface TestData {
+  path: string;
+  name: string;
+  metadata: Metadata;
+  success: boolean;
+  expectedImagePath: string;
+  actualImagePath: string;
+  diffImagePath: string;
+  diff: number;
+}
diff --git a/baremaps-renderer/tsconfig.json b/baremaps-renderer/tsconfig.json
new file mode 100644
index 00000000..78978a07
--- /dev/null
+++ b/baremaps-renderer/tsconfig.json
@@ -0,0 +1,20 @@
+{
+  "compilerOptions": {
+    /* Language and Environment */
+    "target": "es2016" /* Set the JavaScript language version for emitted 
JavaScript and include compatible library declarations. */,
+
+    /* Modules */
+    "module": "commonjs" /* Specify what module code is generated. */,
+    "outDir": "./bin" /* Specify an output folder for all emitted files. */,
+    "esModuleInterop": true /* Emit additional JavaScript to ease support for 
importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for 
type compatibility. */,
+    "forceConsistentCasingInFileNames": true /* Ensure that casing is correct 
in imports. */,
+
+    /* Type Checking */
+    "strict": true /* Enable all strict type-checking options. */,
+    "noImplicitAny": true /* Enable error reporting for expressions and 
declarations with an implied 'any' type. */,
+    "skipLibCheck": true /* Skip type checking all .d.ts files. */,
+    "noUnusedLocals": true
+  },
+  "include": ["src/**/*", "declaration.d.ts"],
+  "exclude": ["node_modules", "bin"]
+}
diff --git a/basemap/tests/.gitignore b/basemap/tests/.gitignore
new file mode 100644
index 00000000..c1775f0a
--- /dev/null
+++ b/basemap/tests/.gitignore
@@ -0,0 +1,2 @@
+**/*.png
+*.html
\ No newline at end of file
diff --git a/basemap/tests/integration/lausanne/metadata.json 
b/basemap/tests/integration/lausanne/metadata.json
new file mode 100644
index 00000000..411cd146
--- /dev/null
+++ b/basemap/tests/integration/lausanne/metadata.json
@@ -0,0 +1,6 @@
+{
+  "width": 512,
+  "height": 512,
+  "center": [6.6323, 46.5197],
+  "zoom": 14
+}

Reply via email to