Add progressive web app companion for cross-platform access

Vite + TypeScript PWA that mirrors the Android app's core features:
- Pre-processed shelter data (build-time UTM33N→WGS84 conversion)
- Leaflet map with shelter markers, user location, and offline tiles
- Canvas compass arrow (ported from DirectionArrowView.kt)
- IndexedDB shelter cache with 7-day staleness check
- Service worker with CacheFirst tiles and precached app shell
- i18n for en, nb, nn (ported from Android strings.xml)
- iOS/Android compass handling with low-pass filter
- Respects user map interaction (no auto-snap on pan/zoom)
- Build revision cache-breaker for reliable SW updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-03-08 17:41:38 +01:00
commit e8428de775
12051 changed files with 1799735 additions and 0 deletions

3
pwa/node_modules/leaflet.offline/.browserslistrc generated vendored Normal file
View file

@ -0,0 +1,3 @@
defaults
not dead
not ie 11

3
pwa/node_modules/leaflet.offline/.commitlintrc.json generated vendored Normal file
View file

@ -0,0 +1,3 @@
{
"extends": ["@commitlint/config-conventional"]
}

28
pwa/node_modules/leaflet.offline/.eslintrc.js generated vendored Normal file
View file

@ -0,0 +1,28 @@
module.exports = {
ignorePatterns: ['/dist'],
env: {
browser: true,
},
extends: ['airbnb-base', 'prettier'],
parserOptions: {
project: './tsconfig.json',
},
overrides: [
{
files: ['src/**/*.ts', 'test/**/*.ts'],
extends: ['airbnb-base', 'airbnb-typescript/base', 'prettier'],
rules: {
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: true,
optionalDependencies: false,
peerDependencies: false,
},
],
'no-underscore-dangle': 0,
'no-return-assign': ['error', 'except-parens'],
},
},
],
};

1
pwa/node_modules/leaflet.offline/.github/FUNDING.yml generated vendored Normal file
View file

@ -0,0 +1 @@
github: [allartk]

View file

@ -0,0 +1,15 @@
version: 2
updates:
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'monthly'
target-branch: 'main'
versioning-strategy: increase-if-necessary
allow:
# Allow both direct and indirect updates for all packages
- dependency-type: "direct"
commit-message:
prefix: fix
prefix-development: chore
include: scope

View file

@ -0,0 +1,33 @@
name: Update docs site
on: workflow_dispatch
permissions:
contents: write # for checkout
pages: write
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 22
- name: Install Dependencies
run: npm ci
- name: Build Docs
run: npm run docs:build
- name: Deploy with gh-pages
run: |
git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git
npm run docs:deploy -- -u "github-actions-bot <support+actions@github.com>"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -0,0 +1,47 @@
name: CI
on:
pull_request: ~
push:
branches:
- main
jobs:
run:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
node-version: 22
check-latest: true
- name: Cache dependencies
id: cache-dependencies
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
if: steps.cache-dependencies.outputs.cache-hit != 'true'
run: npm ci
- name: Make sure puppeteer installs chrome, when using npm cache
if: steps.cache-dependencies.outputs.cache-hit == 'true'
run: npx puppeteer browsers install
- name: Build project
run: npm run build
- name: lint
run: npm run lint
- name: test
run: npm run testci
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4.0.1
with:
token: ${{ secrets.CODECOV_TOKEN }}

View file

@ -0,0 +1,41 @@
name: Release
on: workflow_dispatch
permissions:
id-token: write # Required for OIDC
contents: read # for checkout
jobs:
release:
name: Release
runs-on: ubuntu-latest
permissions:
contents: write # to be able to publish a GitHub release
issues: write # to be able to comment on released issues
pull-requests: write # to be able to comment on released pull requests
id-token: write # to enable use of OIDC for npm provenance
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 22
- name: Install Dependencies
run: npm ci
- name: Build
run: npm run build
- name: Pack
run: npm pack
- name: Semantic Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx semantic-release

1
pwa/node_modules/leaflet.offline/.husky/commit-msg generated vendored Executable file
View file

@ -0,0 +1 @@
npx --no-install commitlint --edit $1

1
pwa/node_modules/leaflet.offline/.husky/pre-commit generated vendored Executable file
View file

@ -0,0 +1 @@
npx lint-staged

1
pwa/node_modules/leaflet.offline/.nvmrc generated vendored Normal file
View file

@ -0,0 +1 @@
lts/jod

2
pwa/node_modules/leaflet.offline/.prettierignore generated vendored Normal file
View file

@ -0,0 +1,2 @@
dist
node_modules

3
pwa/node_modules/leaflet.offline/.prettierrc.json generated vendored Normal file
View file

@ -0,0 +1,3 @@
{
"singleQuote": true
}

18
pwa/node_modules/leaflet.offline/.releaserc.json generated vendored Normal file
View file

@ -0,0 +1,18 @@
{
"branches": [
"main"
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"@semantic-release/github",
{
"assets": [
"leaflet.offline*.tgz"
]
}
],
"@semantic-release/npm"
]
}

165
pwa/node_modules/leaflet.offline/LICENSE generated vendored Normal file
View file

@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

61
pwa/node_modules/leaflet.offline/README.md generated vendored Normal file
View file

@ -0,0 +1,61 @@
# leaflet.offline
[![npm version](https://badge.fury.io/js/leaflet.offline.svg)](https://badge.fury.io/js/leaflet.offline)
[![codecov](https://codecov.io/github/allartk/leaflet.offline/graph/badge.svg?token=dy1uNlSvsh)](https://codecov.io/github/allartk/leaflet.offline)
[![Build Status](https://travis-ci.org/allartk/leaflet.offline.png?branch=master)](https://travis-ci.org/allartk/leaflet.offline)
This library can be used to create maps, which are still available when you are offline and are fast when your connection is not. It works in the browser or can be used in an mobile app based on html and in progressive webapps.
- [examples](https://github.com/allartk/leaflet.offline/tree/main/examples)
## Dependencies
- [Leafletjs](http://leafletjs.com/)
- [idb](https://www.npmjs.com/package/idb) To store the tiles with promises
## Install
### With npm
The package and it's dependencies can also be downloaded into
your existing project with [npm](http://npmjs.com):
```
npm install leaflet.offline
```
In your script add:
```
import 'leaflet.offline'
```
### Manual
Unpack the file for the release (find them under the releasestab ) and add dist/bundle.js in a script tag
to your page (after leaflet and idb).
### Development
For running the example locally, you'll need to clone the project and run:
```
npm i && npm run build
cd docs
npm install && npm run start
```
**pull requests welcome**
* You MUST test your code with `npm test` and `npm run lint`. On wsl wit ubuntu 24.04, install first libasound, libnss3: `sudo apt install libasound2t64 libnss3`, see [puppeteer docs](https://pptr.dev/guides/system-requirements)
* Please one feature at a time, if you can split your PR, please do so.
* Also, do not mix a feature with package updates.
* [Use commit message conventions](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional#rules)
## Api
Generate docs with
```
npm run-script docs
```

10
pwa/node_modules/leaflet.offline/changelog.md generated vendored Normal file
View file

@ -0,0 +1,10 @@
# Version 3
- Removal of setOption method on the control
- getTile is now getBlobByKey
- openTilesDataBase is now an export function to get whole idb database
- New control option alwaysDownload, if false, the control will not redownload tiles already in the db (and the savetile\* event do not fire)
# Version 4
- Changed arguments of getStoredTilesAsJson: send an object with tilesize instead of the full layer

View file

@ -0,0 +1,886 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for ControlSaveTiles.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="prettify.css" />
<link rel="stylesheet" href="base.css" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="index.html">All files</a> ControlSaveTiles.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">83.51% </span>
<span class="quiet">Statements</span>
<span class='fraction'>76/91</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">61.9% </span>
<span class="quiet">Branches</span>
<span class='fraction'>13/21</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">77.27% </span>
<span class="quiet">Functions</span>
<span class='fraction'>17/22</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">82.55% </span>
<span class="quiet">Lines</span>
<span class='fraction'>71/86</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input oninput="onInput()" type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a>
<a name='L103'></a><a href='#L103'>103</a>
<a name='L104'></a><a href='#L104'>104</a>
<a name='L105'></a><a href='#L105'>105</a>
<a name='L106'></a><a href='#L106'>106</a>
<a name='L107'></a><a href='#L107'>107</a>
<a name='L108'></a><a href='#L108'>108</a>
<a name='L109'></a><a href='#L109'>109</a>
<a name='L110'></a><a href='#L110'>110</a>
<a name='L111'></a><a href='#L111'>111</a>
<a name='L112'></a><a href='#L112'>112</a>
<a name='L113'></a><a href='#L113'>113</a>
<a name='L114'></a><a href='#L114'>114</a>
<a name='L115'></a><a href='#L115'>115</a>
<a name='L116'></a><a href='#L116'>116</a>
<a name='L117'></a><a href='#L117'>117</a>
<a name='L118'></a><a href='#L118'>118</a>
<a name='L119'></a><a href='#L119'>119</a>
<a name='L120'></a><a href='#L120'>120</a>
<a name='L121'></a><a href='#L121'>121</a>
<a name='L122'></a><a href='#L122'>122</a>
<a name='L123'></a><a href='#L123'>123</a>
<a name='L124'></a><a href='#L124'>124</a>
<a name='L125'></a><a href='#L125'>125</a>
<a name='L126'></a><a href='#L126'>126</a>
<a name='L127'></a><a href='#L127'>127</a>
<a name='L128'></a><a href='#L128'>128</a>
<a name='L129'></a><a href='#L129'>129</a>
<a name='L130'></a><a href='#L130'>130</a>
<a name='L131'></a><a href='#L131'>131</a>
<a name='L132'></a><a href='#L132'>132</a>
<a name='L133'></a><a href='#L133'>133</a>
<a name='L134'></a><a href='#L134'>134</a>
<a name='L135'></a><a href='#L135'>135</a>
<a name='L136'></a><a href='#L136'>136</a>
<a name='L137'></a><a href='#L137'>137</a>
<a name='L138'></a><a href='#L138'>138</a>
<a name='L139'></a><a href='#L139'>139</a>
<a name='L140'></a><a href='#L140'>140</a>
<a name='L141'></a><a href='#L141'>141</a>
<a name='L142'></a><a href='#L142'>142</a>
<a name='L143'></a><a href='#L143'>143</a>
<a name='L144'></a><a href='#L144'>144</a>
<a name='L145'></a><a href='#L145'>145</a>
<a name='L146'></a><a href='#L146'>146</a>
<a name='L147'></a><a href='#L147'>147</a>
<a name='L148'></a><a href='#L148'>148</a>
<a name='L149'></a><a href='#L149'>149</a>
<a name='L150'></a><a href='#L150'>150</a>
<a name='L151'></a><a href='#L151'>151</a>
<a name='L152'></a><a href='#L152'>152</a>
<a name='L153'></a><a href='#L153'>153</a>
<a name='L154'></a><a href='#L154'>154</a>
<a name='L155'></a><a href='#L155'>155</a>
<a name='L156'></a><a href='#L156'>156</a>
<a name='L157'></a><a href='#L157'>157</a>
<a name='L158'></a><a href='#L158'>158</a>
<a name='L159'></a><a href='#L159'>159</a>
<a name='L160'></a><a href='#L160'>160</a>
<a name='L161'></a><a href='#L161'>161</a>
<a name='L162'></a><a href='#L162'>162</a>
<a name='L163'></a><a href='#L163'>163</a>
<a name='L164'></a><a href='#L164'>164</a>
<a name='L165'></a><a href='#L165'>165</a>
<a name='L166'></a><a href='#L166'>166</a>
<a name='L167'></a><a href='#L167'>167</a>
<a name='L168'></a><a href='#L168'>168</a>
<a name='L169'></a><a href='#L169'>169</a>
<a name='L170'></a><a href='#L170'>170</a>
<a name='L171'></a><a href='#L171'>171</a>
<a name='L172'></a><a href='#L172'>172</a>
<a name='L173'></a><a href='#L173'>173</a>
<a name='L174'></a><a href='#L174'>174</a>
<a name='L175'></a><a href='#L175'>175</a>
<a name='L176'></a><a href='#L176'>176</a>
<a name='L177'></a><a href='#L177'>177</a>
<a name='L178'></a><a href='#L178'>178</a>
<a name='L179'></a><a href='#L179'>179</a>
<a name='L180'></a><a href='#L180'>180</a>
<a name='L181'></a><a href='#L181'>181</a>
<a name='L182'></a><a href='#L182'>182</a>
<a name='L183'></a><a href='#L183'>183</a>
<a name='L184'></a><a href='#L184'>184</a>
<a name='L185'></a><a href='#L185'>185</a>
<a name='L186'></a><a href='#L186'>186</a>
<a name='L187'></a><a href='#L187'>187</a>
<a name='L188'></a><a href='#L188'>188</a>
<a name='L189'></a><a href='#L189'>189</a>
<a name='L190'></a><a href='#L190'>190</a>
<a name='L191'></a><a href='#L191'>191</a>
<a name='L192'></a><a href='#L192'>192</a>
<a name='L193'></a><a href='#L193'>193</a>
<a name='L194'></a><a href='#L194'>194</a>
<a name='L195'></a><a href='#L195'>195</a>
<a name='L196'></a><a href='#L196'>196</a>
<a name='L197'></a><a href='#L197'>197</a>
<a name='L198'></a><a href='#L198'>198</a>
<a name='L199'></a><a href='#L199'>199</a>
<a name='L200'></a><a href='#L200'>200</a>
<a name='L201'></a><a href='#L201'>201</a>
<a name='L202'></a><a href='#L202'>202</a>
<a name='L203'></a><a href='#L203'>203</a>
<a name='L204'></a><a href='#L204'>204</a>
<a name='L205'></a><a href='#L205'>205</a>
<a name='L206'></a><a href='#L206'>206</a>
<a name='L207'></a><a href='#L207'>207</a>
<a name='L208'></a><a href='#L208'>208</a>
<a name='L209'></a><a href='#L209'>209</a>
<a name='L210'></a><a href='#L210'>210</a>
<a name='L211'></a><a href='#L211'>211</a>
<a name='L212'></a><a href='#L212'>212</a>
<a name='L213'></a><a href='#L213'>213</a>
<a name='L214'></a><a href='#L214'>214</a>
<a name='L215'></a><a href='#L215'>215</a>
<a name='L216'></a><a href='#L216'>216</a>
<a name='L217'></a><a href='#L217'>217</a>
<a name='L218'></a><a href='#L218'>218</a>
<a name='L219'></a><a href='#L219'>219</a>
<a name='L220'></a><a href='#L220'>220</a>
<a name='L221'></a><a href='#L221'>221</a>
<a name='L222'></a><a href='#L222'>222</a>
<a name='L223'></a><a href='#L223'>223</a>
<a name='L224'></a><a href='#L224'>224</a>
<a name='L225'></a><a href='#L225'>225</a>
<a name='L226'></a><a href='#L226'>226</a>
<a name='L227'></a><a href='#L227'>227</a>
<a name='L228'></a><a href='#L228'>228</a>
<a name='L229'></a><a href='#L229'>229</a>
<a name='L230'></a><a href='#L230'>230</a>
<a name='L231'></a><a href='#L231'>231</a>
<a name='L232'></a><a href='#L232'>232</a>
<a name='L233'></a><a href='#L233'>233</a>
<a name='L234'></a><a href='#L234'>234</a>
<a name='L235'></a><a href='#L235'>235</a>
<a name='L236'></a><a href='#L236'>236</a>
<a name='L237'></a><a href='#L237'>237</a>
<a name='L238'></a><a href='#L238'>238</a>
<a name='L239'></a><a href='#L239'>239</a>
<a name='L240'></a><a href='#L240'>240</a>
<a name='L241'></a><a href='#L241'>241</a>
<a name='L242'></a><a href='#L242'>242</a>
<a name='L243'></a><a href='#L243'>243</a>
<a name='L244'></a><a href='#L244'>244</a>
<a name='L245'></a><a href='#L245'>245</a>
<a name='L246'></a><a href='#L246'>246</a>
<a name='L247'></a><a href='#L247'>247</a>
<a name='L248'></a><a href='#L248'>248</a>
<a name='L249'></a><a href='#L249'>249</a>
<a name='L250'></a><a href='#L250'>250</a>
<a name='L251'></a><a href='#L251'>251</a>
<a name='L252'></a><a href='#L252'>252</a>
<a name='L253'></a><a href='#L253'>253</a>
<a name='L254'></a><a href='#L254'>254</a>
<a name='L255'></a><a href='#L255'>255</a>
<a name='L256'></a><a href='#L256'>256</a>
<a name='L257'></a><a href='#L257'>257</a>
<a name='L258'></a><a href='#L258'>258</a>
<a name='L259'></a><a href='#L259'>259</a>
<a name='L260'></a><a href='#L260'>260</a>
<a name='L261'></a><a href='#L261'>261</a>
<a name='L262'></a><a href='#L262'>262</a>
<a name='L263'></a><a href='#L263'>263</a>
<a name='L264'></a><a href='#L264'>264</a>
<a name='L265'></a><a href='#L265'>265</a>
<a name='L266'></a><a href='#L266'>266</a>
<a name='L267'></a><a href='#L267'>267</a>
<a name='L268'></a><a href='#L268'>268</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import {
Control,
ControlOptions,
DomEvent,
DomUtil,
LatLngBounds,
bounds,
Map,
} from 'leaflet';
import { TileLayerOffline } from './TileLayerOffline';
import {
truncate,
getStorageLength,
downloadTile,
saveTile,
TileInfo,
hasTile,
} from './TileManager';
&nbsp;
export interface SaveTileOptions extends ControlOptions {
saveText: string;
rmText: string;
maxZoom: number;
saveWhatYouSee: boolean;
bounds: LatLngBounds | null;
confirm: ((status: SaveStatus, successCallback: Function) =&gt; void) | null;
confirmRemoval:
| ((status: SaveStatus, successCallback: Function) =&gt; void)
| null;
parallel: number;
zoomlevels?: number[];
alwaysDownload: boolean;
}
&nbsp;
export interface SaveStatus {
_tilesforSave: TileInfo[];
storagesize: number;
lengthToBeSaved: number;
lengthSaved: number;
lengthLoaded: number;
}
&nbsp;
export class ControlSaveTiles extends Control {
_map!: Map;
&nbsp;
_refocusOnMap!: DomEvent.EventHandlerFn;
&nbsp;
_baseLayer!: TileLayerOffline;
&nbsp;
options: SaveTileOptions;
&nbsp;
status: SaveStatus = {
storagesize: 0,
lengthToBeSaved: 0,
lengthSaved: 0,
lengthLoaded: 0,
_tilesforSave: [],
};
&nbsp;
constructor(baseLayer: TileLayerOffline, options: Partial&lt;SaveTileOptions&gt;) {
super(options);
this._baseLayer = baseLayer;
this.setStorageSize();
this.options = {
...{
position: 'topleft',
saveText: '+',
rmText: '-',
maxZoom: 19,
saveWhatYouSee: false,
bounds: null,
confirm: null,
confirmRemoval: null,
parallel: 50,
zoomlevels: undefined,
alwaysDownload: true,
},
...options,
};
}
&nbsp;
setStorageSize() {
<span class="missing-if-branch" title="if path not taken" >I</span>if (this.status.storagesize) {
<span class="cstat-no" title="statement not covered" > return Promise.resolve(this.status.storagesize);</span>
}
return getStorageLength()
.then((numberOfKeys) =&gt; {
this.status.storagesize = numberOfKeys;
this._baseLayer.fire('storagesize', this.status);
return numberOfKeys;
})
.catch(<span class="fstat-no" title="function not covered" >() =</span>&gt; <span class="cstat-no" title="statement not covered" >0)</span>;
}
&nbsp;
<span class="fstat-no" title="function not covered" > getStorageSize(</span>callback: Function) {
<span class="cstat-no" title="statement not covered" > this.setStorageSize().then(<span class="fstat-no" title="function not covered" >(r</span>esult) =&gt; {</span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (callback) {</span>
<span class="cstat-no" title="statement not covered" > callback(result);</span>
}
});
}
&nbsp;
<span class="fstat-no" title="function not covered" > setLayer(</span>layer: TileLayerOffline) {
<span class="cstat-no" title="statement not covered" > this._baseLayer = layer;</span>
}
&nbsp;
onAdd() {
const container = DomUtil.create('div', 'savetiles leaflet-bar');
const { options } = this;
this._createButton(
options.saveText,
'savetiles',
container,
this._saveTiles,
);
this._createButton(options.rmText, 'rmtiles', container, this._rmTiles);
return container;
}
&nbsp;
_createButton(
html: string,
className: string,
container: HTMLElement,
fn: DomEvent.EventHandlerFn,
) {
const link = DomUtil.create('a', className, container);
link.innerHTML = html;
link.href = '#';
link.ariaRoleDescription = 'button';
&nbsp;
DomEvent.on(link, 'mousedown dblclick', DomEvent.stopPropagation)
.on(link, 'click', DomEvent.stop)
.on(link, 'click', fn, this)
.on(link, 'click', this._refocusOnMap, this);
&nbsp;
return link;
}
&nbsp;
_saveTiles() {
const tiles = this._calculateTiles();
this._resetStatus(tiles);
const successCallback = async () =&gt; {
this._baseLayer.fire('savestart', this.status);
const loader = async (): Promise&lt;void&gt; =&gt; {
const tile = tiles.shift();
if (tile === undefined) {
return Promise.resolve();
}
const blob = await this._loadTile(tile);
if (blob) {
await this._saveTile(tile, blob);
}
return loader();
};
const parallel = Math.min(tiles.length, this.options.parallel);
for (let i = 0; i &lt; parallel; i += 1) {
loader();
}
};
if (this.options.confirm) {
this.options.confirm(this.status, successCallback);
} else {
successCallback();
}
}
&nbsp;
_calculateTiles() {
let tiles: TileInfo[] = [];
// minimum zoom to prevent the user from saving the whole world
const minZoom = 5;
// current zoom or zoom options
let zoomlevels = [];
&nbsp;
if (this.options.saveWhatYouSee) {
const currentZoom = this._map.getZoom();
<span class="missing-if-branch" title="if path not taken" >I</span>if (currentZoom &lt; minZoom) {
<span class="cstat-no" title="statement not covered" > throw new Error(</span>
`It's not possible to save with zoom below level ${minZoom}.`,
);
}
const { maxZoom } = this.options;
&nbsp;
for (let zoom = currentZoom; zoom &lt;= maxZoom; zoom += 1) {
zoomlevels.push(zoom);
}
} else {
zoomlevels = this.options.zoomlevels || [this._map.getZoom()];
}
&nbsp;
const latlngBounds = this.options.bounds || this._map.getBounds();
&nbsp;
for (let i = 0; i &lt; zoomlevels.length; i += 1) {
const area = bounds(
this._map.project(latlngBounds.getNorthWest(), zoomlevels[i]),
this._map.project(latlngBounds.getSouthEast(), zoomlevels[i]),
);
tiles = tiles.concat(this._baseLayer.getTileUrls(area, zoomlevels[i]));
}
return tiles;
}
&nbsp;
_resetStatus(tiles: TileInfo[]) {
this.status = {
lengthLoaded: 0,
lengthToBeSaved: tiles.length,
lengthSaved: 0,
_tilesforSave: tiles,
storagesize: this.status.storagesize,
};
}
&nbsp;
async <span class="fstat-no" title="function not covered" >_loadTile(</span>tile: TileInfo) {
let blob;
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (</span>
this.options.alwaysDownload === true ||
(await hasTile(tile.key)) === false
) {
<span class="cstat-no" title="statement not covered" > blob = await downloadTile(tile.url);</span>
}
&nbsp;
<span class="cstat-no" title="statement not covered" > this.status.lengthLoaded += 1;</span>
&nbsp;
<span class="cstat-no" title="statement not covered" > this._baseLayer.fire('loadtileend', this.status);</span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (this.status.lengthLoaded === this.status.lengthToBeSaved) {</span>
<span class="cstat-no" title="statement not covered" > this._baseLayer.fire('loadend', this.status);</span>
}
<span class="cstat-no" title="statement not covered" > return blob;</span>
}
&nbsp;
async _saveTile(tile: TileInfo, blob: Blob): Promise&lt;void&gt; {
await saveTile(tile, blob);
this.status.lengthSaved += 1;
this._baseLayer.fire('savetileend', this.status);
if (this.status.lengthSaved === this.status.lengthToBeSaved) {
this._baseLayer.fire('saveend', this.status);
this.setStorageSize();
}
}
&nbsp;
_rmTiles() {
const successCallback = () =&gt; {
truncate().then(() =&gt; {
this.status.storagesize = 0;
this._baseLayer.fire('tilesremoved');
this._baseLayer.fire('storagesize', this.status);
});
};
<span class="missing-if-branch" title="if path not taken" >I</span>if (this.options.confirmRemoval) {
<span class="cstat-no" title="statement not covered" > this.options.confirmRemoval(this.status, successCallback);</span>
} else {
successCallback();
}
}
}
&nbsp;
export function savetiles(
baseLayer: TileLayerOffline,
options: Partial&lt;SaveTileOptions&gt;,
) {
return new ControlSaveTiles(baseLayer, options);
}
&nbsp;
/** @ts-ignore */
if (window.L) {
/** @ts-ignore */
window.L.control.savetiles = savetiles;
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-02-10T16:19:18.024Z
</div>
<script src="prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="sorter.js"></script>
<script src="block-navigation.js"></script>
</body>
</html>

View file

@ -0,0 +1,388 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for TileLayerOffline.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="prettify.css" />
<link rel="stylesheet" href="base.css" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="index.html">All files</a> TileLayerOffline.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">90.9% </span>
<span class="quiet">Statements</span>
<span class='fraction'>20/22</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">66.66% </span>
<span class="quiet">Branches</span>
<span class='fraction'>8/12</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">80% </span>
<span class="quiet">Functions</span>
<span class='fraction'>4/5</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">90.47% </span>
<span class="quiet">Lines</span>
<span class='fraction'>19/21</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input oninput="onInput()" type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">90x</span>
<span class="cline-any cline-yes">90x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">90x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import {
Bounds,
Coords,
DomEvent,
DoneCallback,
TileLayer,
TileLayerOptions,
Util,
} from 'leaflet';
import {
getTileUrl,
TileInfo,
getTilePoints,
getTileImageSource,
} from './TileManager';
&nbsp;
export class TileLayerOffline extends TileLayer {
_url!: string;
&nbsp;
createTile(coords: Coords, done: DoneCallback): HTMLElement {
const tile = document.createElement('img');
&nbsp;
DomEvent.on(tile, 'load', Util.bind(this._tileOnLoad, this, done, tile));
DomEvent.on(tile, 'error', Util.bind(this._tileOnError, this, done, tile));
&nbsp;
<span class="missing-if-branch" title="if path not taken" >I</span>if (this.options.crossOrigin || this.options.crossOrigin === '') {
<span class="cstat-no" title="statement not covered" > tile.crossOrigin =</span>
this.options.crossOrigin === true ? '' : this.options.crossOrigin;
}
&nbsp;
tile.alt = '';
&nbsp;
tile.setAttribute('role', 'presentation');
&nbsp;
getTileImageSource(
this._getStorageKey(coords),
this.getTileUrl(coords),
).then((src) =&gt; (tile.src = src));
&nbsp;
return tile;
}
&nbsp;
/**
* get key to use for storage
* @private
* @param {string} url url used to load tile
* @return {string} unique identifier.
*/
_getStorageKey(coords: { x: number; y: number; z: number }) {
return getTileUrl(this._url, {
...coords,
...this.options,
// @ts-ignore: Possibly undefined
s: this.options.subdomains['0'],
});
}
&nbsp;
/**
* Get tileinfo for zoomlevel &amp; bounds
*/
getTileUrls(bounds: Bounds, zoom: number): TileInfo[] {
const tiles: TileInfo[] = [];
const tilePoints = getTilePoints(bounds, this.getTileSize());
for (let index = 0; index &lt; tilePoints.length; index += 1) {
const tilePoint = tilePoints[index];
const data = {
...this.options,
x: tilePoint.x,
y: tilePoint.y,
z: zoom + (this.options.zoomOffset || 0),
};
tiles.push({
key: getTileUrl(this._url, {
...data,
s: this.options.subdomains<span class="branch-0 cbranch-no" title="branch not covered" >?.[0</span>],
}),
url: getTileUrl(this._url, {
...data,
// @ts-ignore: Undefined
s: this._getSubdomain(tilePoint),
}),
z: zoom,
x: tilePoint.x,
y: tilePoint.y,
urlTemplate: this._url,
createdAt: Date.now(),
});
}
return tiles;
}
}
&nbsp;
export function <span class="fstat-no" title="function not covered" >tileLayerOffline(</span>url: string, options: TileLayerOptions) {
<span class="cstat-no" title="statement not covered" > return new TileLayerOffline(url, options);</span>
}
&nbsp;
/** @ts-ignore */
if (window.L) {
/** @ts-ignore */
window.L.tileLayer.offline = tileLayerOffline;
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-02-10T16:19:18.024Z
</div>
<script src="prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="sorter.js"></script>
<script src="block-navigation.js"></script>
</body>
</html>

View file

@ -0,0 +1,763 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for TileManager.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="prettify.css" />
<link rel="stylesheet" href="base.css" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="index.html">All files</a> TileManager.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">98.24% </span>
<span class="quiet">Statements</span>
<span class='fraction'>56/57</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">81.81% </span>
<span class="quiet">Branches</span>
<span class='fraction'>9/11</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>15/15</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">98.14% </span>
<span class="quiet">Lines</span>
<span class='fraction'>53/54</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input oninput="onInput()" type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a>
<a name='L103'></a><a href='#L103'>103</a>
<a name='L104'></a><a href='#L104'>104</a>
<a name='L105'></a><a href='#L105'>105</a>
<a name='L106'></a><a href='#L106'>106</a>
<a name='L107'></a><a href='#L107'>107</a>
<a name='L108'></a><a href='#L108'>108</a>
<a name='L109'></a><a href='#L109'>109</a>
<a name='L110'></a><a href='#L110'>110</a>
<a name='L111'></a><a href='#L111'>111</a>
<a name='L112'></a><a href='#L112'>112</a>
<a name='L113'></a><a href='#L113'>113</a>
<a name='L114'></a><a href='#L114'>114</a>
<a name='L115'></a><a href='#L115'>115</a>
<a name='L116'></a><a href='#L116'>116</a>
<a name='L117'></a><a href='#L117'>117</a>
<a name='L118'></a><a href='#L118'>118</a>
<a name='L119'></a><a href='#L119'>119</a>
<a name='L120'></a><a href='#L120'>120</a>
<a name='L121'></a><a href='#L121'>121</a>
<a name='L122'></a><a href='#L122'>122</a>
<a name='L123'></a><a href='#L123'>123</a>
<a name='L124'></a><a href='#L124'>124</a>
<a name='L125'></a><a href='#L125'>125</a>
<a name='L126'></a><a href='#L126'>126</a>
<a name='L127'></a><a href='#L127'>127</a>
<a name='L128'></a><a href='#L128'>128</a>
<a name='L129'></a><a href='#L129'>129</a>
<a name='L130'></a><a href='#L130'>130</a>
<a name='L131'></a><a href='#L131'>131</a>
<a name='L132'></a><a href='#L132'>132</a>
<a name='L133'></a><a href='#L133'>133</a>
<a name='L134'></a><a href='#L134'>134</a>
<a name='L135'></a><a href='#L135'>135</a>
<a name='L136'></a><a href='#L136'>136</a>
<a name='L137'></a><a href='#L137'>137</a>
<a name='L138'></a><a href='#L138'>138</a>
<a name='L139'></a><a href='#L139'>139</a>
<a name='L140'></a><a href='#L140'>140</a>
<a name='L141'></a><a href='#L141'>141</a>
<a name='L142'></a><a href='#L142'>142</a>
<a name='L143'></a><a href='#L143'>143</a>
<a name='L144'></a><a href='#L144'>144</a>
<a name='L145'></a><a href='#L145'>145</a>
<a name='L146'></a><a href='#L146'>146</a>
<a name='L147'></a><a href='#L147'>147</a>
<a name='L148'></a><a href='#L148'>148</a>
<a name='L149'></a><a href='#L149'>149</a>
<a name='L150'></a><a href='#L150'>150</a>
<a name='L151'></a><a href='#L151'>151</a>
<a name='L152'></a><a href='#L152'>152</a>
<a name='L153'></a><a href='#L153'>153</a>
<a name='L154'></a><a href='#L154'>154</a>
<a name='L155'></a><a href='#L155'>155</a>
<a name='L156'></a><a href='#L156'>156</a>
<a name='L157'></a><a href='#L157'>157</a>
<a name='L158'></a><a href='#L158'>158</a>
<a name='L159'></a><a href='#L159'>159</a>
<a name='L160'></a><a href='#L160'>160</a>
<a name='L161'></a><a href='#L161'>161</a>
<a name='L162'></a><a href='#L162'>162</a>
<a name='L163'></a><a href='#L163'>163</a>
<a name='L164'></a><a href='#L164'>164</a>
<a name='L165'></a><a href='#L165'>165</a>
<a name='L166'></a><a href='#L166'>166</a>
<a name='L167'></a><a href='#L167'>167</a>
<a name='L168'></a><a href='#L168'>168</a>
<a name='L169'></a><a href='#L169'>169</a>
<a name='L170'></a><a href='#L170'>170</a>
<a name='L171'></a><a href='#L171'>171</a>
<a name='L172'></a><a href='#L172'>172</a>
<a name='L173'></a><a href='#L173'>173</a>
<a name='L174'></a><a href='#L174'>174</a>
<a name='L175'></a><a href='#L175'>175</a>
<a name='L176'></a><a href='#L176'>176</a>
<a name='L177'></a><a href='#L177'>177</a>
<a name='L178'></a><a href='#L178'>178</a>
<a name='L179'></a><a href='#L179'>179</a>
<a name='L180'></a><a href='#L180'>180</a>
<a name='L181'></a><a href='#L181'>181</a>
<a name='L182'></a><a href='#L182'>182</a>
<a name='L183'></a><a href='#L183'>183</a>
<a name='L184'></a><a href='#L184'>184</a>
<a name='L185'></a><a href='#L185'>185</a>
<a name='L186'></a><a href='#L186'>186</a>
<a name='L187'></a><a href='#L187'>187</a>
<a name='L188'></a><a href='#L188'>188</a>
<a name='L189'></a><a href='#L189'>189</a>
<a name='L190'></a><a href='#L190'>190</a>
<a name='L191'></a><a href='#L191'>191</a>
<a name='L192'></a><a href='#L192'>192</a>
<a name='L193'></a><a href='#L193'>193</a>
<a name='L194'></a><a href='#L194'>194</a>
<a name='L195'></a><a href='#L195'>195</a>
<a name='L196'></a><a href='#L196'>196</a>
<a name='L197'></a><a href='#L197'>197</a>
<a name='L198'></a><a href='#L198'>198</a>
<a name='L199'></a><a href='#L199'>199</a>
<a name='L200'></a><a href='#L200'>200</a>
<a name='L201'></a><a href='#L201'>201</a>
<a name='L202'></a><a href='#L202'>202</a>
<a name='L203'></a><a href='#L203'>203</a>
<a name='L204'></a><a href='#L204'>204</a>
<a name='L205'></a><a href='#L205'>205</a>
<a name='L206'></a><a href='#L206'>206</a>
<a name='L207'></a><a href='#L207'>207</a>
<a name='L208'></a><a href='#L208'>208</a>
<a name='L209'></a><a href='#L209'>209</a>
<a name='L210'></a><a href='#L210'>210</a>
<a name='L211'></a><a href='#L211'>211</a>
<a name='L212'></a><a href='#L212'>212</a>
<a name='L213'></a><a href='#L213'>213</a>
<a name='L214'></a><a href='#L214'>214</a>
<a name='L215'></a><a href='#L215'>215</a>
<a name='L216'></a><a href='#L216'>216</a>
<a name='L217'></a><a href='#L217'>217</a>
<a name='L218'></a><a href='#L218'>218</a>
<a name='L219'></a><a href='#L219'>219</a>
<a name='L220'></a><a href='#L220'>220</a>
<a name='L221'></a><a href='#L221'>221</a>
<a name='L222'></a><a href='#L222'>222</a>
<a name='L223'></a><a href='#L223'>223</a>
<a name='L224'></a><a href='#L224'>224</a>
<a name='L225'></a><a href='#L225'>225</a>
<a name='L226'></a><a href='#L226'>226</a>
<a name='L227'></a><a href='#L227'>227</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">69x</span>
<span class="cline-any cline-yes">66x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">193x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">91x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">12x</span>
<span class="cline-any cline-yes">12x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">/**
* Api methods used in control and layer
* For advanced usage
*
* @module TileManager
*
*/
&nbsp;
import { Bounds, Browser, CRS, Point, Util } from 'leaflet';
import { openDB, deleteDB, IDBPDatabase } from 'idb';
import { FeatureCollection, Polygon } from 'geojson';
&nbsp;
export type TileInfo = {
key: string;
url: string;
urlTemplate: string;
x: number;
y: number;
z: number;
createdAt: number;
};
&nbsp;
export type StoredTile = TileInfo &amp; { blob: Blob };
&nbsp;
const tileStoreName = 'tileStore';
const urlTemplateIndex = 'urlTemplate';
let dbPromise: Promise&lt;IDBPDatabase&gt; | undefined;
&nbsp;
export function openTilesDataBase(): Promise&lt;IDBPDatabase&gt; {
if (dbPromise) {
return dbPromise;
}
dbPromise = openDB('leaflet.offline', 2, {
upgrade(db, oldVersion) {
deleteDB('leaflet_offline');
deleteDB('leaflet_offline_areas');
&nbsp;
if (oldVersion &lt; 1) {
const tileStore = db.createObjectStore(tileStoreName, {
keyPath: 'key',
});
tileStore.createIndex(urlTemplateIndex, 'urlTemplate');
tileStore.createIndex('z', 'z');
}
},
});
return dbPromise;
}
&nbsp;
/**
* @example
* ```js
* import { getStorageLength } from 'leaflet.offline'
* getStorageLength().then(i =&gt; console.log(i + 'tiles in storage'))
* ```
*/
export async function getStorageLength(): Promise&lt;number&gt; {
const db = await openTilesDataBase();
return db.count(tileStoreName);
}
&nbsp;
/**
* @example
* ```js
* import { getStorageInfo } from 'leaflet.offline'
* getStorageInfo('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png')
* ```
*/
export async function getStorageInfo(
urlTemplate: string,
): Promise&lt;StoredTile[]&gt; {
const range = IDBKeyRange.only(urlTemplate);
const db = await openTilesDataBase();
return db.getAllFromIndex(tileStoreName, urlTemplateIndex, range);
}
&nbsp;
/**
* @example
* ```js
* import { downloadTile } from 'leaflet.offline'
* downloadTile(tileInfo.url).then(blob =&gt; saveTile(tileInfo, blob))
* ```
*/
export async function downloadTile(tileUrl: string): Promise&lt;Blob&gt; {
const response = await fetch(tileUrl);
if (!response.ok) {
throw new Error(`Request failed with status ${response.statusText}`);
}
return response.blob();
}
/**
* @example
* ```js
* saveTile(tileInfo, blob).then(() =&gt; console.log(`saved tile from ${tileInfo.url}`))
* ```
*/
export async function saveTile(
tileInfo: TileInfo,
blob: Blob,
): Promise&lt;IDBValidKey&gt; {
const db = await openTilesDataBase();
return db.put(tileStoreName, {
blob,
...tileInfo,
});
}
&nbsp;
export function getTileUrl(urlTemplate: string, data: any): string {
return Util.template(urlTemplate, {
...data,
r: Browser.retina ? <span class="branch-0 cbranch-no" title="branch not covered" >'@2x' </span>: '',
});
}
&nbsp;
export function getTilePoints(area: Bounds, tileSize: Point): Point[] {
const points: Point[] = [];
<span class="missing-if-branch" title="if path not taken" >I</span>if (!area.min || !area.max) {
<span class="cstat-no" title="statement not covered" > return points;</span>
}
const topLeftTile = area.min.divideBy(tileSize.x).floor();
const bottomRightTile = area.max.divideBy(tileSize.x).floor();
&nbsp;
for (let j = topLeftTile.y; j &lt;= bottomRightTile.y; j += 1) {
for (let i = topLeftTile.x; i &lt;= bottomRightTile.x; i += 1) {
points.push(new Point(i, j));
}
}
return points;
}
/**
* Get a geojson of tiles from one resource
*
* @example
* const urlTemplate = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
* const getGeoJsonData = () =&gt; LeafletOffline.getStorageInfo(urlTemplate)
* .then((data) =&gt; LeafletOffline.getStoredTilesAsJson(baseLayer, data));
*
* getGeoJsonData().then((geojson) =&gt; {
* storageLayer = L.geoJSON(geojson).bindPopup(
* (clickedLayer) =&gt; clickedLayer.feature.properties.key,
* );
* });
*
*/
export function getStoredTilesAsJson(
tileSize: { x: number; y: number },
tiles: TileInfo[],
): FeatureCollection&lt;Polygon&gt; {
const featureCollection: FeatureCollection&lt;Polygon&gt; = {
type: 'FeatureCollection',
features: [],
};
for (let i = 0; i &lt; tiles.length; i += 1) {
const topLeftPoint = new Point(
tiles[i].x * tileSize.x,
tiles[i].y * tileSize.y,
);
const bottomRightPoint = new Point(
topLeftPoint.x + tileSize.x,
topLeftPoint.y + tileSize.y,
);
&nbsp;
const topLeftlatlng = CRS.EPSG3857.pointToLatLng(topLeftPoint, tiles[i].z);
const botRightlatlng = CRS.EPSG3857.pointToLatLng(
bottomRightPoint,
tiles[i].z,
);
featureCollection.features.push({
type: 'Feature',
properties: tiles[i],
geometry: {
type: 'Polygon',
coordinates: [
[
[topLeftlatlng.lng, topLeftlatlng.lat],
[botRightlatlng.lng, topLeftlatlng.lat],
[botRightlatlng.lng, botRightlatlng.lat],
[topLeftlatlng.lng, botRightlatlng.lat],
[topLeftlatlng.lng, topLeftlatlng.lat],
],
],
},
});
}
&nbsp;
return featureCollection;
}
&nbsp;
/**
* Remove tile by key
*/
export async function removeTile(key: string): Promise&lt;void&gt; {
const db = await openTilesDataBase();
return db.delete(tileStoreName, key);
}
&nbsp;
/**
* Get single tile blob
*/
export async function getBlobByKey(key: string): Promise&lt;Blob&gt; {
return (await openTilesDataBase())
.get(tileStoreName, key)
.then((result) =&gt; result &amp;&amp; result.blob);
}
&nbsp;
export async function hasTile(key: string): Promise&lt;boolean&gt; {
const db = await openTilesDataBase();
const result = await db.getKey(tileStoreName, key);
return result !== undefined;
}
&nbsp;
/**
* Remove everything
*/
export async function truncate(): Promise&lt;void&gt; {
return (await openTilesDataBase()).clear(tileStoreName);
}
&nbsp;
export async function getTileImageSource(key: string, url: string) {
const shouldUseUrl = !(await hasTile(key));
if (shouldUseUrl) {
return url;
}
const blob = await getBlobByKey(key);
return URL.createObjectURL(blob);
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-02-10T16:19:18.024Z
</div>
<script src="prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="sorter.js"></script>
<script src="block-navigation.js"></script>
</body>
</html>

View file

@ -0,0 +1,224 @@
body, html {
margin:0; padding: 0;
height: 100%;
}
body {
font-family: Helvetica Neue, Helvetica, Arial;
font-size: 14px;
color:#333;
}
.small { font-size: 12px; }
*, *:after, *:before {
-webkit-box-sizing:border-box;
-moz-box-sizing:border-box;
box-sizing:border-box;
}
h1 { font-size: 20px; margin: 0;}
h2 { font-size: 14px; }
pre {
font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
margin: 0;
padding: 0;
-moz-tab-size: 2;
-o-tab-size: 2;
tab-size: 2;
}
a { color:#0074D9; text-decoration:none; }
a:hover { text-decoration:underline; }
.strong { font-weight: bold; }
.space-top1 { padding: 10px 0 0 0; }
.pad2y { padding: 20px 0; }
.pad1y { padding: 10px 0; }
.pad2x { padding: 0 20px; }
.pad2 { padding: 20px; }
.pad1 { padding: 10px; }
.space-left2 { padding-left:55px; }
.space-right2 { padding-right:20px; }
.center { text-align:center; }
.clearfix { display:block; }
.clearfix:after {
content:'';
display:block;
height:0;
clear:both;
visibility:hidden;
}
.fl { float: left; }
@media only screen and (max-width:640px) {
.col3 { width:100%; max-width:100%; }
.hide-mobile { display:none!important; }
}
.quiet {
color: #7f7f7f;
color: rgba(0,0,0,0.5);
}
.quiet a { opacity: 0.7; }
.fraction {
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
font-size: 10px;
color: #555;
background: #E8E8E8;
padding: 4px 5px;
border-radius: 3px;
vertical-align: middle;
}
div.path a:link, div.path a:visited { color: #333; }
table.coverage {
border-collapse: collapse;
margin: 10px 0 0 0;
padding: 0;
}
table.coverage td {
margin: 0;
padding: 0;
vertical-align: top;
}
table.coverage td.line-count {
text-align: right;
padding: 0 5px 0 20px;
}
table.coverage td.line-coverage {
text-align: right;
padding-right: 10px;
min-width:20px;
}
table.coverage td span.cline-any {
display: inline-block;
padding: 0 5px;
width: 100%;
}
.missing-if-branch {
display: inline-block;
margin-right: 5px;
border-radius: 3px;
position: relative;
padding: 0 4px;
background: #333;
color: yellow;
}
.skip-if-branch {
display: none;
margin-right: 10px;
position: relative;
padding: 0 4px;
background: #ccc;
color: white;
}
.missing-if-branch .typ, .skip-if-branch .typ {
color: inherit !important;
}
.coverage-summary {
border-collapse: collapse;
width: 100%;
}
.coverage-summary tr { border-bottom: 1px solid #bbb; }
.keyline-all { border: 1px solid #ddd; }
.coverage-summary td, .coverage-summary th { padding: 10px; }
.coverage-summary tbody { border: 1px solid #bbb; }
.coverage-summary td { border-right: 1px solid #bbb; }
.coverage-summary td:last-child { border-right: none; }
.coverage-summary th {
text-align: left;
font-weight: normal;
white-space: nowrap;
}
.coverage-summary th.file { border-right: none !important; }
.coverage-summary th.pct { }
.coverage-summary th.pic,
.coverage-summary th.abs,
.coverage-summary td.pct,
.coverage-summary td.abs { text-align: right; }
.coverage-summary td.file { white-space: nowrap; }
.coverage-summary td.pic { min-width: 120px !important; }
.coverage-summary tfoot td { }
.coverage-summary .sorter {
height: 10px;
width: 7px;
display: inline-block;
margin-left: 0.5em;
background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
}
.coverage-summary .sorted .sorter {
background-position: 0 -20px;
}
.coverage-summary .sorted-desc .sorter {
background-position: 0 -10px;
}
.status-line { height: 10px; }
/* yellow */
.cbranch-no { background: yellow !important; color: #111; }
/* dark red */
.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
.low .chart { border:1px solid #C21F39 }
.highlighted,
.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
background: #C21F39 !important;
}
/* medium red */
.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
/* light red */
.low, .cline-no { background:#FCE1E5 }
/* light green */
.high, .cline-yes { background:rgb(230,245,208) }
/* medium green */
.cstat-yes { background:rgb(161,215,106) }
/* dark green */
.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
.high .chart { border:1px solid rgb(77,146,33) }
/* dark yellow (gold) */
.status-line.medium, .medium .cover-fill { background: #f9cd0b; }
.medium .chart { border:1px solid #f9cd0b; }
/* light yellow */
.medium { background: #fff4c2; }
.cstat-skip { background: #ddd; color: #111; }
.fstat-skip { background: #ddd; color: #111 !important; }
.cbranch-skip { background: #ddd !important; color: #111; }
span.cline-neutral { background: #eaeaea; }
.coverage-summary td.empty {
opacity: .5;
padding-top: 4px;
padding-bottom: 4px;
line-height: 1;
color: #888;
}
.cover-fill, .cover-empty {
display:inline-block;
height: 12px;
}
.chart {
line-height: 0;
}
.cover-empty {
background: white;
}
.cover-full {
border-right: none !important;
}
pre.prettyprint {
border: none !important;
padding: 0 !important;
margin: 0 !important;
}
.com { color: #999 !important; }
.ignore-none { color: #999; font-weight: normal; }
.wrapper {
min-height: 100%;
height: auto !important;
height: 100%;
margin: 0 auto -48px;
}
.footer, .push {
height: 48px;
}

View file

@ -0,0 +1,87 @@
/* eslint-disable */
var jumpToCode = (function init() {
// Classes of code we would like to highlight in the file view
var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
// Elements to highlight in the file listing view
var fileListingElements = ['td.pct.low'];
// We don't want to select elements that are direct descendants of another match
var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
// Selecter that finds elements on the page to which we can jump
var selector =
fileListingElements.join(', ') +
', ' +
notSelector +
missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
// The NodeList of matching elements
var missingCoverageElements = document.querySelectorAll(selector);
var currentIndex;
function toggleClass(index) {
missingCoverageElements
.item(currentIndex)
.classList.remove('highlighted');
missingCoverageElements.item(index).classList.add('highlighted');
}
function makeCurrent(index) {
toggleClass(index);
currentIndex = index;
missingCoverageElements.item(index).scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
});
}
function goToPrevious() {
var nextIndex = 0;
if (typeof currentIndex !== 'number' || currentIndex === 0) {
nextIndex = missingCoverageElements.length - 1;
} else if (missingCoverageElements.length > 1) {
nextIndex = currentIndex - 1;
}
makeCurrent(nextIndex);
}
function goToNext() {
var nextIndex = 0;
if (
typeof currentIndex === 'number' &&
currentIndex < missingCoverageElements.length - 1
) {
nextIndex = currentIndex + 1;
}
makeCurrent(nextIndex);
}
return function jump(event) {
if (
document.getElementById('fileSearch') === document.activeElement &&
document.activeElement != null
) {
// if we're currently focused on the search input, we don't want to navigate
return;
}
switch (event.which) {
case 78: // n
case 74: // j
goToNext();
break;
case 66: // b
case 75: // k
case 80: // p
goToPrevious();
break;
}
};
})();
window.addEventListener('keydown', jumpToCode);

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 B

View file

@ -0,0 +1,146 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for All files</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="prettify.css" />
<link rel="stylesheet" href="base.css" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1>All files</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">89.41% </span>
<span class="quiet">Statements</span>
<span class='fraction'>152/170</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">68.18% </span>
<span class="quiet">Branches</span>
<span class='fraction'>30/44</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">85.71% </span>
<span class="quiet">Functions</span>
<span class='fraction'>36/42</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">88.81% </span>
<span class="quiet">Lines</span>
<span class='fraction'>143/161</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input oninput="onInput()" type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody><tr>
<td class="file high" data-value="ControlSaveTiles.ts"><a href="ControlSaveTiles.ts.html">ControlSaveTiles.ts</a></td>
<td data-value="83.51" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 83%"></div><div class="cover-empty" style="width: 17%"></div></div>
</td>
<td data-value="83.51" class="pct high">83.51%</td>
<td data-value="91" class="abs high">76/91</td>
<td data-value="61.9" class="pct medium">61.9%</td>
<td data-value="21" class="abs medium">13/21</td>
<td data-value="77.27" class="pct medium">77.27%</td>
<td data-value="22" class="abs medium">17/22</td>
<td data-value="82.55" class="pct high">82.55%</td>
<td data-value="86" class="abs high">71/86</td>
</tr>
<tr>
<td class="file high" data-value="TileLayerOffline.ts"><a href="TileLayerOffline.ts.html">TileLayerOffline.ts</a></td>
<td data-value="90.9" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 90%"></div><div class="cover-empty" style="width: 10%"></div></div>
</td>
<td data-value="90.9" class="pct high">90.9%</td>
<td data-value="22" class="abs high">20/22</td>
<td data-value="66.66" class="pct medium">66.66%</td>
<td data-value="12" class="abs medium">8/12</td>
<td data-value="80" class="pct high">80%</td>
<td data-value="5" class="abs high">4/5</td>
<td data-value="90.47" class="pct high">90.47%</td>
<td data-value="21" class="abs high">19/21</td>
</tr>
<tr>
<td class="file high" data-value="TileManager.ts"><a href="TileManager.ts.html">TileManager.ts</a></td>
<td data-value="98.24" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 98%"></div><div class="cover-empty" style="width: 2%"></div></div>
</td>
<td data-value="98.24" class="pct high">98.24%</td>
<td data-value="57" class="abs high">56/57</td>
<td data-value="81.81" class="pct high">81.81%</td>
<td data-value="11" class="abs high">9/11</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="15" class="abs high">15/15</td>
<td data-value="98.14" class="pct high">98.14%</td>
<td data-value="54" class="abs high">53/54</td>
</tr>
</tbody>
</table>
</div>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-02-10T16:19:18.024Z
</div>
<script src="prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="sorter.js"></script>
<script src="block-navigation.js"></script>
</body>
</html>

View file

@ -0,0 +1,886 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for ControlSaveTiles.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="prettify.css" />
<link rel="stylesheet" href="base.css" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="index.html">All files</a> ControlSaveTiles.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">83.51% </span>
<span class="quiet">Statements</span>
<span class='fraction'>76/91</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">61.9% </span>
<span class="quiet">Branches</span>
<span class='fraction'>13/21</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">77.27% </span>
<span class="quiet">Functions</span>
<span class='fraction'>17/22</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">82.55% </span>
<span class="quiet">Lines</span>
<span class='fraction'>71/86</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input oninput="onInput()" type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a>
<a name='L103'></a><a href='#L103'>103</a>
<a name='L104'></a><a href='#L104'>104</a>
<a name='L105'></a><a href='#L105'>105</a>
<a name='L106'></a><a href='#L106'>106</a>
<a name='L107'></a><a href='#L107'>107</a>
<a name='L108'></a><a href='#L108'>108</a>
<a name='L109'></a><a href='#L109'>109</a>
<a name='L110'></a><a href='#L110'>110</a>
<a name='L111'></a><a href='#L111'>111</a>
<a name='L112'></a><a href='#L112'>112</a>
<a name='L113'></a><a href='#L113'>113</a>
<a name='L114'></a><a href='#L114'>114</a>
<a name='L115'></a><a href='#L115'>115</a>
<a name='L116'></a><a href='#L116'>116</a>
<a name='L117'></a><a href='#L117'>117</a>
<a name='L118'></a><a href='#L118'>118</a>
<a name='L119'></a><a href='#L119'>119</a>
<a name='L120'></a><a href='#L120'>120</a>
<a name='L121'></a><a href='#L121'>121</a>
<a name='L122'></a><a href='#L122'>122</a>
<a name='L123'></a><a href='#L123'>123</a>
<a name='L124'></a><a href='#L124'>124</a>
<a name='L125'></a><a href='#L125'>125</a>
<a name='L126'></a><a href='#L126'>126</a>
<a name='L127'></a><a href='#L127'>127</a>
<a name='L128'></a><a href='#L128'>128</a>
<a name='L129'></a><a href='#L129'>129</a>
<a name='L130'></a><a href='#L130'>130</a>
<a name='L131'></a><a href='#L131'>131</a>
<a name='L132'></a><a href='#L132'>132</a>
<a name='L133'></a><a href='#L133'>133</a>
<a name='L134'></a><a href='#L134'>134</a>
<a name='L135'></a><a href='#L135'>135</a>
<a name='L136'></a><a href='#L136'>136</a>
<a name='L137'></a><a href='#L137'>137</a>
<a name='L138'></a><a href='#L138'>138</a>
<a name='L139'></a><a href='#L139'>139</a>
<a name='L140'></a><a href='#L140'>140</a>
<a name='L141'></a><a href='#L141'>141</a>
<a name='L142'></a><a href='#L142'>142</a>
<a name='L143'></a><a href='#L143'>143</a>
<a name='L144'></a><a href='#L144'>144</a>
<a name='L145'></a><a href='#L145'>145</a>
<a name='L146'></a><a href='#L146'>146</a>
<a name='L147'></a><a href='#L147'>147</a>
<a name='L148'></a><a href='#L148'>148</a>
<a name='L149'></a><a href='#L149'>149</a>
<a name='L150'></a><a href='#L150'>150</a>
<a name='L151'></a><a href='#L151'>151</a>
<a name='L152'></a><a href='#L152'>152</a>
<a name='L153'></a><a href='#L153'>153</a>
<a name='L154'></a><a href='#L154'>154</a>
<a name='L155'></a><a href='#L155'>155</a>
<a name='L156'></a><a href='#L156'>156</a>
<a name='L157'></a><a href='#L157'>157</a>
<a name='L158'></a><a href='#L158'>158</a>
<a name='L159'></a><a href='#L159'>159</a>
<a name='L160'></a><a href='#L160'>160</a>
<a name='L161'></a><a href='#L161'>161</a>
<a name='L162'></a><a href='#L162'>162</a>
<a name='L163'></a><a href='#L163'>163</a>
<a name='L164'></a><a href='#L164'>164</a>
<a name='L165'></a><a href='#L165'>165</a>
<a name='L166'></a><a href='#L166'>166</a>
<a name='L167'></a><a href='#L167'>167</a>
<a name='L168'></a><a href='#L168'>168</a>
<a name='L169'></a><a href='#L169'>169</a>
<a name='L170'></a><a href='#L170'>170</a>
<a name='L171'></a><a href='#L171'>171</a>
<a name='L172'></a><a href='#L172'>172</a>
<a name='L173'></a><a href='#L173'>173</a>
<a name='L174'></a><a href='#L174'>174</a>
<a name='L175'></a><a href='#L175'>175</a>
<a name='L176'></a><a href='#L176'>176</a>
<a name='L177'></a><a href='#L177'>177</a>
<a name='L178'></a><a href='#L178'>178</a>
<a name='L179'></a><a href='#L179'>179</a>
<a name='L180'></a><a href='#L180'>180</a>
<a name='L181'></a><a href='#L181'>181</a>
<a name='L182'></a><a href='#L182'>182</a>
<a name='L183'></a><a href='#L183'>183</a>
<a name='L184'></a><a href='#L184'>184</a>
<a name='L185'></a><a href='#L185'>185</a>
<a name='L186'></a><a href='#L186'>186</a>
<a name='L187'></a><a href='#L187'>187</a>
<a name='L188'></a><a href='#L188'>188</a>
<a name='L189'></a><a href='#L189'>189</a>
<a name='L190'></a><a href='#L190'>190</a>
<a name='L191'></a><a href='#L191'>191</a>
<a name='L192'></a><a href='#L192'>192</a>
<a name='L193'></a><a href='#L193'>193</a>
<a name='L194'></a><a href='#L194'>194</a>
<a name='L195'></a><a href='#L195'>195</a>
<a name='L196'></a><a href='#L196'>196</a>
<a name='L197'></a><a href='#L197'>197</a>
<a name='L198'></a><a href='#L198'>198</a>
<a name='L199'></a><a href='#L199'>199</a>
<a name='L200'></a><a href='#L200'>200</a>
<a name='L201'></a><a href='#L201'>201</a>
<a name='L202'></a><a href='#L202'>202</a>
<a name='L203'></a><a href='#L203'>203</a>
<a name='L204'></a><a href='#L204'>204</a>
<a name='L205'></a><a href='#L205'>205</a>
<a name='L206'></a><a href='#L206'>206</a>
<a name='L207'></a><a href='#L207'>207</a>
<a name='L208'></a><a href='#L208'>208</a>
<a name='L209'></a><a href='#L209'>209</a>
<a name='L210'></a><a href='#L210'>210</a>
<a name='L211'></a><a href='#L211'>211</a>
<a name='L212'></a><a href='#L212'>212</a>
<a name='L213'></a><a href='#L213'>213</a>
<a name='L214'></a><a href='#L214'>214</a>
<a name='L215'></a><a href='#L215'>215</a>
<a name='L216'></a><a href='#L216'>216</a>
<a name='L217'></a><a href='#L217'>217</a>
<a name='L218'></a><a href='#L218'>218</a>
<a name='L219'></a><a href='#L219'>219</a>
<a name='L220'></a><a href='#L220'>220</a>
<a name='L221'></a><a href='#L221'>221</a>
<a name='L222'></a><a href='#L222'>222</a>
<a name='L223'></a><a href='#L223'>223</a>
<a name='L224'></a><a href='#L224'>224</a>
<a name='L225'></a><a href='#L225'>225</a>
<a name='L226'></a><a href='#L226'>226</a>
<a name='L227'></a><a href='#L227'>227</a>
<a name='L228'></a><a href='#L228'>228</a>
<a name='L229'></a><a href='#L229'>229</a>
<a name='L230'></a><a href='#L230'>230</a>
<a name='L231'></a><a href='#L231'>231</a>
<a name='L232'></a><a href='#L232'>232</a>
<a name='L233'></a><a href='#L233'>233</a>
<a name='L234'></a><a href='#L234'>234</a>
<a name='L235'></a><a href='#L235'>235</a>
<a name='L236'></a><a href='#L236'>236</a>
<a name='L237'></a><a href='#L237'>237</a>
<a name='L238'></a><a href='#L238'>238</a>
<a name='L239'></a><a href='#L239'>239</a>
<a name='L240'></a><a href='#L240'>240</a>
<a name='L241'></a><a href='#L241'>241</a>
<a name='L242'></a><a href='#L242'>242</a>
<a name='L243'></a><a href='#L243'>243</a>
<a name='L244'></a><a href='#L244'>244</a>
<a name='L245'></a><a href='#L245'>245</a>
<a name='L246'></a><a href='#L246'>246</a>
<a name='L247'></a><a href='#L247'>247</a>
<a name='L248'></a><a href='#L248'>248</a>
<a name='L249'></a><a href='#L249'>249</a>
<a name='L250'></a><a href='#L250'>250</a>
<a name='L251'></a><a href='#L251'>251</a>
<a name='L252'></a><a href='#L252'>252</a>
<a name='L253'></a><a href='#L253'>253</a>
<a name='L254'></a><a href='#L254'>254</a>
<a name='L255'></a><a href='#L255'>255</a>
<a name='L256'></a><a href='#L256'>256</a>
<a name='L257'></a><a href='#L257'>257</a>
<a name='L258'></a><a href='#L258'>258</a>
<a name='L259'></a><a href='#L259'>259</a>
<a name='L260'></a><a href='#L260'>260</a>
<a name='L261'></a><a href='#L261'>261</a>
<a name='L262'></a><a href='#L262'>262</a>
<a name='L263'></a><a href='#L263'>263</a>
<a name='L264'></a><a href='#L264'>264</a>
<a name='L265'></a><a href='#L265'>265</a>
<a name='L266'></a><a href='#L266'>266</a>
<a name='L267'></a><a href='#L267'>267</a>
<a name='L268'></a><a href='#L268'>268</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">20x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">18x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">4x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">6x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">5x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">8x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">9x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import {
Control,
ControlOptions,
DomEvent,
DomUtil,
LatLngBounds,
bounds,
Map,
} from 'leaflet';
import { TileLayerOffline } from './TileLayerOffline';
import {
truncate,
getStorageLength,
downloadTile,
saveTile,
TileInfo,
hasTile,
} from './TileManager';
&nbsp;
export interface SaveTileOptions extends ControlOptions {
saveText: string;
rmText: string;
maxZoom: number;
saveWhatYouSee: boolean;
bounds: LatLngBounds | null;
confirm: ((status: SaveStatus, successCallback: Function) =&gt; void) | null;
confirmRemoval:
| ((status: SaveStatus, successCallback: Function) =&gt; void)
| null;
parallel: number;
zoomlevels?: number[];
alwaysDownload: boolean;
}
&nbsp;
export interface SaveStatus {
_tilesforSave: TileInfo[];
storagesize: number;
lengthToBeSaved: number;
lengthSaved: number;
lengthLoaded: number;
}
&nbsp;
export class ControlSaveTiles extends Control {
_map!: Map;
&nbsp;
_refocusOnMap!: DomEvent.EventHandlerFn;
&nbsp;
_baseLayer!: TileLayerOffline;
&nbsp;
options: SaveTileOptions;
&nbsp;
status: SaveStatus = {
storagesize: 0,
lengthToBeSaved: 0,
lengthSaved: 0,
lengthLoaded: 0,
_tilesforSave: [],
};
&nbsp;
constructor(baseLayer: TileLayerOffline, options: Partial&lt;SaveTileOptions&gt;) {
super(options);
this._baseLayer = baseLayer;
this.setStorageSize();
this.options = {
...{
position: 'topleft',
saveText: '+',
rmText: '-',
maxZoom: 19,
saveWhatYouSee: false,
bounds: null,
confirm: null,
confirmRemoval: null,
parallel: 50,
zoomlevels: undefined,
alwaysDownload: true,
},
...options,
};
}
&nbsp;
setStorageSize() {
<span class="missing-if-branch" title="if path not taken" >I</span>if (this.status.storagesize) {
<span class="cstat-no" title="statement not covered" > return Promise.resolve(this.status.storagesize);</span>
}
return getStorageLength()
.then((numberOfKeys) =&gt; {
this.status.storagesize = numberOfKeys;
this._baseLayer.fire('storagesize', this.status);
return numberOfKeys;
})
.catch(<span class="fstat-no" title="function not covered" >() =</span>&gt; <span class="cstat-no" title="statement not covered" >0)</span>;
}
&nbsp;
<span class="fstat-no" title="function not covered" > getStorageSize(</span>callback: Function) {
<span class="cstat-no" title="statement not covered" > this.setStorageSize().then(<span class="fstat-no" title="function not covered" >(r</span>esult) =&gt; {</span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (callback) {</span>
<span class="cstat-no" title="statement not covered" > callback(result);</span>
}
});
}
&nbsp;
<span class="fstat-no" title="function not covered" > setLayer(</span>layer: TileLayerOffline) {
<span class="cstat-no" title="statement not covered" > this._baseLayer = layer;</span>
}
&nbsp;
onAdd() {
const container = DomUtil.create('div', 'savetiles leaflet-bar');
const { options } = this;
this._createButton(
options.saveText,
'savetiles',
container,
this._saveTiles,
);
this._createButton(options.rmText, 'rmtiles', container, this._rmTiles);
return container;
}
&nbsp;
_createButton(
html: string,
className: string,
container: HTMLElement,
fn: DomEvent.EventHandlerFn,
) {
const link = DomUtil.create('a', className, container);
link.innerHTML = html;
link.href = '#';
link.ariaRoleDescription = 'button';
&nbsp;
DomEvent.on(link, 'mousedown dblclick', DomEvent.stopPropagation)
.on(link, 'click', DomEvent.stop)
.on(link, 'click', fn, this)
.on(link, 'click', this._refocusOnMap, this);
&nbsp;
return link;
}
&nbsp;
_saveTiles() {
const tiles = this._calculateTiles();
this._resetStatus(tiles);
const successCallback = async () =&gt; {
this._baseLayer.fire('savestart', this.status);
const loader = async (): Promise&lt;void&gt; =&gt; {
const tile = tiles.shift();
if (tile === undefined) {
return Promise.resolve();
}
const blob = await this._loadTile(tile);
if (blob) {
await this._saveTile(tile, blob);
}
return loader();
};
const parallel = Math.min(tiles.length, this.options.parallel);
for (let i = 0; i &lt; parallel; i += 1) {
loader();
}
};
if (this.options.confirm) {
this.options.confirm(this.status, successCallback);
} else {
successCallback();
}
}
&nbsp;
_calculateTiles() {
let tiles: TileInfo[] = [];
// minimum zoom to prevent the user from saving the whole world
const minZoom = 5;
// current zoom or zoom options
let zoomlevels = [];
&nbsp;
if (this.options.saveWhatYouSee) {
const currentZoom = this._map.getZoom();
<span class="missing-if-branch" title="if path not taken" >I</span>if (currentZoom &lt; minZoom) {
<span class="cstat-no" title="statement not covered" > throw new Error(</span>
`It's not possible to save with zoom below level ${minZoom}.`,
);
}
const { maxZoom } = this.options;
&nbsp;
for (let zoom = currentZoom; zoom &lt;= maxZoom; zoom += 1) {
zoomlevels.push(zoom);
}
} else {
zoomlevels = this.options.zoomlevels || [this._map.getZoom()];
}
&nbsp;
const latlngBounds = this.options.bounds || this._map.getBounds();
&nbsp;
for (let i = 0; i &lt; zoomlevels.length; i += 1) {
const area = bounds(
this._map.project(latlngBounds.getNorthWest(), zoomlevels[i]),
this._map.project(latlngBounds.getSouthEast(), zoomlevels[i]),
);
tiles = tiles.concat(this._baseLayer.getTileUrls(area, zoomlevels[i]));
}
return tiles;
}
&nbsp;
_resetStatus(tiles: TileInfo[]) {
this.status = {
lengthLoaded: 0,
lengthToBeSaved: tiles.length,
lengthSaved: 0,
_tilesforSave: tiles,
storagesize: this.status.storagesize,
};
}
&nbsp;
async <span class="fstat-no" title="function not covered" >_loadTile(</span>tile: TileInfo) {
let blob;
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (</span>
this.options.alwaysDownload === true ||
(await hasTile(tile.key)) === false
) {
<span class="cstat-no" title="statement not covered" > blob = await downloadTile(tile.url);</span>
}
&nbsp;
<span class="cstat-no" title="statement not covered" > this.status.lengthLoaded += 1;</span>
&nbsp;
<span class="cstat-no" title="statement not covered" > this._baseLayer.fire('loadtileend', this.status);</span>
<span class="cstat-no" title="statement not covered" > <span class="missing-if-branch" title="if path not taken" >I</span>if (this.status.lengthLoaded === this.status.lengthToBeSaved) {</span>
<span class="cstat-no" title="statement not covered" > this._baseLayer.fire('loadend', this.status);</span>
}
<span class="cstat-no" title="statement not covered" > return blob;</span>
}
&nbsp;
async _saveTile(tile: TileInfo, blob: Blob): Promise&lt;void&gt; {
await saveTile(tile, blob);
this.status.lengthSaved += 1;
this._baseLayer.fire('savetileend', this.status);
if (this.status.lengthSaved === this.status.lengthToBeSaved) {
this._baseLayer.fire('saveend', this.status);
this.setStorageSize();
}
}
&nbsp;
_rmTiles() {
const successCallback = () =&gt; {
truncate().then(() =&gt; {
this.status.storagesize = 0;
this._baseLayer.fire('tilesremoved');
this._baseLayer.fire('storagesize', this.status);
});
};
<span class="missing-if-branch" title="if path not taken" >I</span>if (this.options.confirmRemoval) {
<span class="cstat-no" title="statement not covered" > this.options.confirmRemoval(this.status, successCallback);</span>
} else {
successCallback();
}
}
}
&nbsp;
export function savetiles(
baseLayer: TileLayerOffline,
options: Partial&lt;SaveTileOptions&gt;,
) {
return new ControlSaveTiles(baseLayer, options);
}
&nbsp;
/** @ts-ignore */
if (window.L) {
/** @ts-ignore */
window.L.control.savetiles = savetiles;
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-02-10T16:19:18.025Z
</div>
<script src="prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="sorter.js"></script>
<script src="block-navigation.js"></script>
</body>
</html>

View file

@ -0,0 +1,388 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for TileLayerOffline.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="prettify.css" />
<link rel="stylesheet" href="base.css" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="index.html">All files</a> TileLayerOffline.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">90.9% </span>
<span class="quiet">Statements</span>
<span class='fraction'>20/22</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">66.66% </span>
<span class="quiet">Branches</span>
<span class='fraction'>8/12</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">80% </span>
<span class="quiet">Functions</span>
<span class='fraction'>4/5</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">90.47% </span>
<span class="quiet">Lines</span>
<span class='fraction'>19/21</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input oninput="onInput()" type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">10x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">13x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-yes">90x</span>
<span class="cline-any cline-yes">90x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">90x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">15x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import {
Bounds,
Coords,
DomEvent,
DoneCallback,
TileLayer,
TileLayerOptions,
Util,
} from 'leaflet';
import {
getTileUrl,
TileInfo,
getTilePoints,
getTileImageSource,
} from './TileManager';
&nbsp;
export class TileLayerOffline extends TileLayer {
_url!: string;
&nbsp;
createTile(coords: Coords, done: DoneCallback): HTMLElement {
const tile = document.createElement('img');
&nbsp;
DomEvent.on(tile, 'load', Util.bind(this._tileOnLoad, this, done, tile));
DomEvent.on(tile, 'error', Util.bind(this._tileOnError, this, done, tile));
&nbsp;
<span class="missing-if-branch" title="if path not taken" >I</span>if (this.options.crossOrigin || this.options.crossOrigin === '') {
<span class="cstat-no" title="statement not covered" > tile.crossOrigin =</span>
this.options.crossOrigin === true ? '' : this.options.crossOrigin;
}
&nbsp;
tile.alt = '';
&nbsp;
tile.setAttribute('role', 'presentation');
&nbsp;
getTileImageSource(
this._getStorageKey(coords),
this.getTileUrl(coords),
).then((src) =&gt; (tile.src = src));
&nbsp;
return tile;
}
&nbsp;
/**
* get key to use for storage
* @private
* @param {string} url url used to load tile
* @return {string} unique identifier.
*/
_getStorageKey(coords: { x: number; y: number; z: number }) {
return getTileUrl(this._url, {
...coords,
...this.options,
// @ts-ignore: Possibly undefined
s: this.options.subdomains['0'],
});
}
&nbsp;
/**
* Get tileinfo for zoomlevel &amp; bounds
*/
getTileUrls(bounds: Bounds, zoom: number): TileInfo[] {
const tiles: TileInfo[] = [];
const tilePoints = getTilePoints(bounds, this.getTileSize());
for (let index = 0; index &lt; tilePoints.length; index += 1) {
const tilePoint = tilePoints[index];
const data = {
...this.options,
x: tilePoint.x,
y: tilePoint.y,
z: zoom + (this.options.zoomOffset || 0),
};
tiles.push({
key: getTileUrl(this._url, {
...data,
s: this.options.subdomains<span class="branch-0 cbranch-no" title="branch not covered" >?.[0</span>],
}),
url: getTileUrl(this._url, {
...data,
// @ts-ignore: Undefined
s: this._getSubdomain(tilePoint),
}),
z: zoom,
x: tilePoint.x,
y: tilePoint.y,
urlTemplate: this._url,
createdAt: Date.now(),
});
}
return tiles;
}
}
&nbsp;
export function <span class="fstat-no" title="function not covered" >tileLayerOffline(</span>url: string, options: TileLayerOptions) {
<span class="cstat-no" title="statement not covered" > return new TileLayerOffline(url, options);</span>
}
&nbsp;
/** @ts-ignore */
if (window.L) {
/** @ts-ignore */
window.L.tileLayer.offline = tileLayerOffline;
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-02-10T16:19:18.025Z
</div>
<script src="prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="sorter.js"></script>
<script src="block-navigation.js"></script>
</body>
</html>

View file

@ -0,0 +1,763 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for TileManager.ts</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="prettify.css" />
<link rel="stylesheet" href="base.css" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="index.html">All files</a> TileManager.ts</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">98.24% </span>
<span class="quiet">Statements</span>
<span class='fraction'>56/57</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">81.81% </span>
<span class="quiet">Branches</span>
<span class='fraction'>9/11</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>15/15</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">98.14% </span>
<span class="quiet">Lines</span>
<span class='fraction'>53/54</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input oninput="onInput()" type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a>
<a name='L33'></a><a href='#L33'>33</a>
<a name='L34'></a><a href='#L34'>34</a>
<a name='L35'></a><a href='#L35'>35</a>
<a name='L36'></a><a href='#L36'>36</a>
<a name='L37'></a><a href='#L37'>37</a>
<a name='L38'></a><a href='#L38'>38</a>
<a name='L39'></a><a href='#L39'>39</a>
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
<a name='L43'></a><a href='#L43'>43</a>
<a name='L44'></a><a href='#L44'>44</a>
<a name='L45'></a><a href='#L45'>45</a>
<a name='L46'></a><a href='#L46'>46</a>
<a name='L47'></a><a href='#L47'>47</a>
<a name='L48'></a><a href='#L48'>48</a>
<a name='L49'></a><a href='#L49'>49</a>
<a name='L50'></a><a href='#L50'>50</a>
<a name='L51'></a><a href='#L51'>51</a>
<a name='L52'></a><a href='#L52'>52</a>
<a name='L53'></a><a href='#L53'>53</a>
<a name='L54'></a><a href='#L54'>54</a>
<a name='L55'></a><a href='#L55'>55</a>
<a name='L56'></a><a href='#L56'>56</a>
<a name='L57'></a><a href='#L57'>57</a>
<a name='L58'></a><a href='#L58'>58</a>
<a name='L59'></a><a href='#L59'>59</a>
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
<a name='L63'></a><a href='#L63'>63</a>
<a name='L64'></a><a href='#L64'>64</a>
<a name='L65'></a><a href='#L65'>65</a>
<a name='L66'></a><a href='#L66'>66</a>
<a name='L67'></a><a href='#L67'>67</a>
<a name='L68'></a><a href='#L68'>68</a>
<a name='L69'></a><a href='#L69'>69</a>
<a name='L70'></a><a href='#L70'>70</a>
<a name='L71'></a><a href='#L71'>71</a>
<a name='L72'></a><a href='#L72'>72</a>
<a name='L73'></a><a href='#L73'>73</a>
<a name='L74'></a><a href='#L74'>74</a>
<a name='L75'></a><a href='#L75'>75</a>
<a name='L76'></a><a href='#L76'>76</a>
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
<a name='L80'></a><a href='#L80'>80</a>
<a name='L81'></a><a href='#L81'>81</a>
<a name='L82'></a><a href='#L82'>82</a>
<a name='L83'></a><a href='#L83'>83</a>
<a name='L84'></a><a href='#L84'>84</a>
<a name='L85'></a><a href='#L85'>85</a>
<a name='L86'></a><a href='#L86'>86</a>
<a name='L87'></a><a href='#L87'>87</a>
<a name='L88'></a><a href='#L88'>88</a>
<a name='L89'></a><a href='#L89'>89</a>
<a name='L90'></a><a href='#L90'>90</a>
<a name='L91'></a><a href='#L91'>91</a>
<a name='L92'></a><a href='#L92'>92</a>
<a name='L93'></a><a href='#L93'>93</a>
<a name='L94'></a><a href='#L94'>94</a>
<a name='L95'></a><a href='#L95'>95</a>
<a name='L96'></a><a href='#L96'>96</a>
<a name='L97'></a><a href='#L97'>97</a>
<a name='L98'></a><a href='#L98'>98</a>
<a name='L99'></a><a href='#L99'>99</a>
<a name='L100'></a><a href='#L100'>100</a>
<a name='L101'></a><a href='#L101'>101</a>
<a name='L102'></a><a href='#L102'>102</a>
<a name='L103'></a><a href='#L103'>103</a>
<a name='L104'></a><a href='#L104'>104</a>
<a name='L105'></a><a href='#L105'>105</a>
<a name='L106'></a><a href='#L106'>106</a>
<a name='L107'></a><a href='#L107'>107</a>
<a name='L108'></a><a href='#L108'>108</a>
<a name='L109'></a><a href='#L109'>109</a>
<a name='L110'></a><a href='#L110'>110</a>
<a name='L111'></a><a href='#L111'>111</a>
<a name='L112'></a><a href='#L112'>112</a>
<a name='L113'></a><a href='#L113'>113</a>
<a name='L114'></a><a href='#L114'>114</a>
<a name='L115'></a><a href='#L115'>115</a>
<a name='L116'></a><a href='#L116'>116</a>
<a name='L117'></a><a href='#L117'>117</a>
<a name='L118'></a><a href='#L118'>118</a>
<a name='L119'></a><a href='#L119'>119</a>
<a name='L120'></a><a href='#L120'>120</a>
<a name='L121'></a><a href='#L121'>121</a>
<a name='L122'></a><a href='#L122'>122</a>
<a name='L123'></a><a href='#L123'>123</a>
<a name='L124'></a><a href='#L124'>124</a>
<a name='L125'></a><a href='#L125'>125</a>
<a name='L126'></a><a href='#L126'>126</a>
<a name='L127'></a><a href='#L127'>127</a>
<a name='L128'></a><a href='#L128'>128</a>
<a name='L129'></a><a href='#L129'>129</a>
<a name='L130'></a><a href='#L130'>130</a>
<a name='L131'></a><a href='#L131'>131</a>
<a name='L132'></a><a href='#L132'>132</a>
<a name='L133'></a><a href='#L133'>133</a>
<a name='L134'></a><a href='#L134'>134</a>
<a name='L135'></a><a href='#L135'>135</a>
<a name='L136'></a><a href='#L136'>136</a>
<a name='L137'></a><a href='#L137'>137</a>
<a name='L138'></a><a href='#L138'>138</a>
<a name='L139'></a><a href='#L139'>139</a>
<a name='L140'></a><a href='#L140'>140</a>
<a name='L141'></a><a href='#L141'>141</a>
<a name='L142'></a><a href='#L142'>142</a>
<a name='L143'></a><a href='#L143'>143</a>
<a name='L144'></a><a href='#L144'>144</a>
<a name='L145'></a><a href='#L145'>145</a>
<a name='L146'></a><a href='#L146'>146</a>
<a name='L147'></a><a href='#L147'>147</a>
<a name='L148'></a><a href='#L148'>148</a>
<a name='L149'></a><a href='#L149'>149</a>
<a name='L150'></a><a href='#L150'>150</a>
<a name='L151'></a><a href='#L151'>151</a>
<a name='L152'></a><a href='#L152'>152</a>
<a name='L153'></a><a href='#L153'>153</a>
<a name='L154'></a><a href='#L154'>154</a>
<a name='L155'></a><a href='#L155'>155</a>
<a name='L156'></a><a href='#L156'>156</a>
<a name='L157'></a><a href='#L157'>157</a>
<a name='L158'></a><a href='#L158'>158</a>
<a name='L159'></a><a href='#L159'>159</a>
<a name='L160'></a><a href='#L160'>160</a>
<a name='L161'></a><a href='#L161'>161</a>
<a name='L162'></a><a href='#L162'>162</a>
<a name='L163'></a><a href='#L163'>163</a>
<a name='L164'></a><a href='#L164'>164</a>
<a name='L165'></a><a href='#L165'>165</a>
<a name='L166'></a><a href='#L166'>166</a>
<a name='L167'></a><a href='#L167'>167</a>
<a name='L168'></a><a href='#L168'>168</a>
<a name='L169'></a><a href='#L169'>169</a>
<a name='L170'></a><a href='#L170'>170</a>
<a name='L171'></a><a href='#L171'>171</a>
<a name='L172'></a><a href='#L172'>172</a>
<a name='L173'></a><a href='#L173'>173</a>
<a name='L174'></a><a href='#L174'>174</a>
<a name='L175'></a><a href='#L175'>175</a>
<a name='L176'></a><a href='#L176'>176</a>
<a name='L177'></a><a href='#L177'>177</a>
<a name='L178'></a><a href='#L178'>178</a>
<a name='L179'></a><a href='#L179'>179</a>
<a name='L180'></a><a href='#L180'>180</a>
<a name='L181'></a><a href='#L181'>181</a>
<a name='L182'></a><a href='#L182'>182</a>
<a name='L183'></a><a href='#L183'>183</a>
<a name='L184'></a><a href='#L184'>184</a>
<a name='L185'></a><a href='#L185'>185</a>
<a name='L186'></a><a href='#L186'>186</a>
<a name='L187'></a><a href='#L187'>187</a>
<a name='L188'></a><a href='#L188'>188</a>
<a name='L189'></a><a href='#L189'>189</a>
<a name='L190'></a><a href='#L190'>190</a>
<a name='L191'></a><a href='#L191'>191</a>
<a name='L192'></a><a href='#L192'>192</a>
<a name='L193'></a><a href='#L193'>193</a>
<a name='L194'></a><a href='#L194'>194</a>
<a name='L195'></a><a href='#L195'>195</a>
<a name='L196'></a><a href='#L196'>196</a>
<a name='L197'></a><a href='#L197'>197</a>
<a name='L198'></a><a href='#L198'>198</a>
<a name='L199'></a><a href='#L199'>199</a>
<a name='L200'></a><a href='#L200'>200</a>
<a name='L201'></a><a href='#L201'>201</a>
<a name='L202'></a><a href='#L202'>202</a>
<a name='L203'></a><a href='#L203'>203</a>
<a name='L204'></a><a href='#L204'>204</a>
<a name='L205'></a><a href='#L205'>205</a>
<a name='L206'></a><a href='#L206'>206</a>
<a name='L207'></a><a href='#L207'>207</a>
<a name='L208'></a><a href='#L208'>208</a>
<a name='L209'></a><a href='#L209'>209</a>
<a name='L210'></a><a href='#L210'>210</a>
<a name='L211'></a><a href='#L211'>211</a>
<a name='L212'></a><a href='#L212'>212</a>
<a name='L213'></a><a href='#L213'>213</a>
<a name='L214'></a><a href='#L214'>214</a>
<a name='L215'></a><a href='#L215'>215</a>
<a name='L216'></a><a href='#L216'>216</a>
<a name='L217'></a><a href='#L217'>217</a>
<a name='L218'></a><a href='#L218'>218</a>
<a name='L219'></a><a href='#L219'>219</a>
<a name='L220'></a><a href='#L220'>220</a>
<a name='L221'></a><a href='#L221'>221</a>
<a name='L222'></a><a href='#L222'>222</a>
<a name='L223'></a><a href='#L223'>223</a>
<a name='L224'></a><a href='#L224'>224</a>
<a name='L225'></a><a href='#L225'>225</a>
<a name='L226'></a><a href='#L226'>226</a>
<a name='L227'></a><a href='#L227'>227</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">69x</span>
<span class="cline-any cline-yes">66x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-yes">3x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">193x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-yes">31x</span>
<span class="cline-any cline-yes">91x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">16x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-yes">14x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">22x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">12x</span>
<span class="cline-any cline-yes">12x</span>
<span class="cline-any cline-yes">11x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">/**
* Api methods used in control and layer
* For advanced usage
*
* @module TileManager
*
*/
&nbsp;
import { Bounds, Browser, CRS, Point, Util } from 'leaflet';
import { openDB, deleteDB, IDBPDatabase } from 'idb';
import { FeatureCollection, Polygon } from 'geojson';
&nbsp;
export type TileInfo = {
key: string;
url: string;
urlTemplate: string;
x: number;
y: number;
z: number;
createdAt: number;
};
&nbsp;
export type StoredTile = TileInfo &amp; { blob: Blob };
&nbsp;
const tileStoreName = 'tileStore';
const urlTemplateIndex = 'urlTemplate';
let dbPromise: Promise&lt;IDBPDatabase&gt; | undefined;
&nbsp;
export function openTilesDataBase(): Promise&lt;IDBPDatabase&gt; {
if (dbPromise) {
return dbPromise;
}
dbPromise = openDB('leaflet.offline', 2, {
upgrade(db, oldVersion) {
deleteDB('leaflet_offline');
deleteDB('leaflet_offline_areas');
&nbsp;
if (oldVersion &lt; 1) {
const tileStore = db.createObjectStore(tileStoreName, {
keyPath: 'key',
});
tileStore.createIndex(urlTemplateIndex, 'urlTemplate');
tileStore.createIndex('z', 'z');
}
},
});
return dbPromise;
}
&nbsp;
/**
* @example
* ```js
* import { getStorageLength } from 'leaflet.offline'
* getStorageLength().then(i =&gt; console.log(i + 'tiles in storage'))
* ```
*/
export async function getStorageLength(): Promise&lt;number&gt; {
const db = await openTilesDataBase();
return db.count(tileStoreName);
}
&nbsp;
/**
* @example
* ```js
* import { getStorageInfo } from 'leaflet.offline'
* getStorageInfo('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png')
* ```
*/
export async function getStorageInfo(
urlTemplate: string,
): Promise&lt;StoredTile[]&gt; {
const range = IDBKeyRange.only(urlTemplate);
const db = await openTilesDataBase();
return db.getAllFromIndex(tileStoreName, urlTemplateIndex, range);
}
&nbsp;
/**
* @example
* ```js
* import { downloadTile } from 'leaflet.offline'
* downloadTile(tileInfo.url).then(blob =&gt; saveTile(tileInfo, blob))
* ```
*/
export async function downloadTile(tileUrl: string): Promise&lt;Blob&gt; {
const response = await fetch(tileUrl);
if (!response.ok) {
throw new Error(`Request failed with status ${response.statusText}`);
}
return response.blob();
}
/**
* @example
* ```js
* saveTile(tileInfo, blob).then(() =&gt; console.log(`saved tile from ${tileInfo.url}`))
* ```
*/
export async function saveTile(
tileInfo: TileInfo,
blob: Blob,
): Promise&lt;IDBValidKey&gt; {
const db = await openTilesDataBase();
return db.put(tileStoreName, {
blob,
...tileInfo,
});
}
&nbsp;
export function getTileUrl(urlTemplate: string, data: any): string {
return Util.template(urlTemplate, {
...data,
r: Browser.retina ? <span class="branch-0 cbranch-no" title="branch not covered" >'@2x' </span>: '',
});
}
&nbsp;
export function getTilePoints(area: Bounds, tileSize: Point): Point[] {
const points: Point[] = [];
<span class="missing-if-branch" title="if path not taken" >I</span>if (!area.min || !area.max) {
<span class="cstat-no" title="statement not covered" > return points;</span>
}
const topLeftTile = area.min.divideBy(tileSize.x).floor();
const bottomRightTile = area.max.divideBy(tileSize.x).floor();
&nbsp;
for (let j = topLeftTile.y; j &lt;= bottomRightTile.y; j += 1) {
for (let i = topLeftTile.x; i &lt;= bottomRightTile.x; i += 1) {
points.push(new Point(i, j));
}
}
return points;
}
/**
* Get a geojson of tiles from one resource
*
* @example
* const urlTemplate = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
* const getGeoJsonData = () =&gt; LeafletOffline.getStorageInfo(urlTemplate)
* .then((data) =&gt; LeafletOffline.getStoredTilesAsJson(baseLayer, data));
*
* getGeoJsonData().then((geojson) =&gt; {
* storageLayer = L.geoJSON(geojson).bindPopup(
* (clickedLayer) =&gt; clickedLayer.feature.properties.key,
* );
* });
*
*/
export function getStoredTilesAsJson(
tileSize: { x: number; y: number },
tiles: TileInfo[],
): FeatureCollection&lt;Polygon&gt; {
const featureCollection: FeatureCollection&lt;Polygon&gt; = {
type: 'FeatureCollection',
features: [],
};
for (let i = 0; i &lt; tiles.length; i += 1) {
const topLeftPoint = new Point(
tiles[i].x * tileSize.x,
tiles[i].y * tileSize.y,
);
const bottomRightPoint = new Point(
topLeftPoint.x + tileSize.x,
topLeftPoint.y + tileSize.y,
);
&nbsp;
const topLeftlatlng = CRS.EPSG3857.pointToLatLng(topLeftPoint, tiles[i].z);
const botRightlatlng = CRS.EPSG3857.pointToLatLng(
bottomRightPoint,
tiles[i].z,
);
featureCollection.features.push({
type: 'Feature',
properties: tiles[i],
geometry: {
type: 'Polygon',
coordinates: [
[
[topLeftlatlng.lng, topLeftlatlng.lat],
[botRightlatlng.lng, topLeftlatlng.lat],
[botRightlatlng.lng, botRightlatlng.lat],
[topLeftlatlng.lng, botRightlatlng.lat],
[topLeftlatlng.lng, topLeftlatlng.lat],
],
],
},
});
}
&nbsp;
return featureCollection;
}
&nbsp;
/**
* Remove tile by key
*/
export async function removeTile(key: string): Promise&lt;void&gt; {
const db = await openTilesDataBase();
return db.delete(tileStoreName, key);
}
&nbsp;
/**
* Get single tile blob
*/
export async function getBlobByKey(key: string): Promise&lt;Blob&gt; {
return (await openTilesDataBase())
.get(tileStoreName, key)
.then((result) =&gt; result &amp;&amp; result.blob);
}
&nbsp;
export async function hasTile(key: string): Promise&lt;boolean&gt; {
const db = await openTilesDataBase();
const result = await db.getKey(tileStoreName, key);
return result !== undefined;
}
&nbsp;
/**
* Remove everything
*/
export async function truncate(): Promise&lt;void&gt; {
return (await openTilesDataBase()).clear(tileStoreName);
}
&nbsp;
export async function getTileImageSource(key: string, url: string) {
const shouldUseUrl = !(await hasTile(key));
if (shouldUseUrl) {
return url;
}
const blob = await getBlobByKey(key);
return URL.createObjectURL(blob);
}
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-02-10T16:19:18.025Z
</div>
<script src="prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="sorter.js"></script>
<script src="block-navigation.js"></script>
</body>
</html>

View file

@ -0,0 +1,224 @@
body, html {
margin:0; padding: 0;
height: 100%;
}
body {
font-family: Helvetica Neue, Helvetica, Arial;
font-size: 14px;
color:#333;
}
.small { font-size: 12px; }
*, *:after, *:before {
-webkit-box-sizing:border-box;
-moz-box-sizing:border-box;
box-sizing:border-box;
}
h1 { font-size: 20px; margin: 0;}
h2 { font-size: 14px; }
pre {
font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
margin: 0;
padding: 0;
-moz-tab-size: 2;
-o-tab-size: 2;
tab-size: 2;
}
a { color:#0074D9; text-decoration:none; }
a:hover { text-decoration:underline; }
.strong { font-weight: bold; }
.space-top1 { padding: 10px 0 0 0; }
.pad2y { padding: 20px 0; }
.pad1y { padding: 10px 0; }
.pad2x { padding: 0 20px; }
.pad2 { padding: 20px; }
.pad1 { padding: 10px; }
.space-left2 { padding-left:55px; }
.space-right2 { padding-right:20px; }
.center { text-align:center; }
.clearfix { display:block; }
.clearfix:after {
content:'';
display:block;
height:0;
clear:both;
visibility:hidden;
}
.fl { float: left; }
@media only screen and (max-width:640px) {
.col3 { width:100%; max-width:100%; }
.hide-mobile { display:none!important; }
}
.quiet {
color: #7f7f7f;
color: rgba(0,0,0,0.5);
}
.quiet a { opacity: 0.7; }
.fraction {
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
font-size: 10px;
color: #555;
background: #E8E8E8;
padding: 4px 5px;
border-radius: 3px;
vertical-align: middle;
}
div.path a:link, div.path a:visited { color: #333; }
table.coverage {
border-collapse: collapse;
margin: 10px 0 0 0;
padding: 0;
}
table.coverage td {
margin: 0;
padding: 0;
vertical-align: top;
}
table.coverage td.line-count {
text-align: right;
padding: 0 5px 0 20px;
}
table.coverage td.line-coverage {
text-align: right;
padding-right: 10px;
min-width:20px;
}
table.coverage td span.cline-any {
display: inline-block;
padding: 0 5px;
width: 100%;
}
.missing-if-branch {
display: inline-block;
margin-right: 5px;
border-radius: 3px;
position: relative;
padding: 0 4px;
background: #333;
color: yellow;
}
.skip-if-branch {
display: none;
margin-right: 10px;
position: relative;
padding: 0 4px;
background: #ccc;
color: white;
}
.missing-if-branch .typ, .skip-if-branch .typ {
color: inherit !important;
}
.coverage-summary {
border-collapse: collapse;
width: 100%;
}
.coverage-summary tr { border-bottom: 1px solid #bbb; }
.keyline-all { border: 1px solid #ddd; }
.coverage-summary td, .coverage-summary th { padding: 10px; }
.coverage-summary tbody { border: 1px solid #bbb; }
.coverage-summary td { border-right: 1px solid #bbb; }
.coverage-summary td:last-child { border-right: none; }
.coverage-summary th {
text-align: left;
font-weight: normal;
white-space: nowrap;
}
.coverage-summary th.file { border-right: none !important; }
.coverage-summary th.pct { }
.coverage-summary th.pic,
.coverage-summary th.abs,
.coverage-summary td.pct,
.coverage-summary td.abs { text-align: right; }
.coverage-summary td.file { white-space: nowrap; }
.coverage-summary td.pic { min-width: 120px !important; }
.coverage-summary tfoot td { }
.coverage-summary .sorter {
height: 10px;
width: 7px;
display: inline-block;
margin-left: 0.5em;
background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
}
.coverage-summary .sorted .sorter {
background-position: 0 -20px;
}
.coverage-summary .sorted-desc .sorter {
background-position: 0 -10px;
}
.status-line { height: 10px; }
/* yellow */
.cbranch-no { background: yellow !important; color: #111; }
/* dark red */
.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
.low .chart { border:1px solid #C21F39 }
.highlighted,
.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
background: #C21F39 !important;
}
/* medium red */
.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
/* light red */
.low, .cline-no { background:#FCE1E5 }
/* light green */
.high, .cline-yes { background:rgb(230,245,208) }
/* medium green */
.cstat-yes { background:rgb(161,215,106) }
/* dark green */
.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
.high .chart { border:1px solid rgb(77,146,33) }
/* dark yellow (gold) */
.status-line.medium, .medium .cover-fill { background: #f9cd0b; }
.medium .chart { border:1px solid #f9cd0b; }
/* light yellow */
.medium { background: #fff4c2; }
.cstat-skip { background: #ddd; color: #111; }
.fstat-skip { background: #ddd; color: #111 !important; }
.cbranch-skip { background: #ddd !important; color: #111; }
span.cline-neutral { background: #eaeaea; }
.coverage-summary td.empty {
opacity: .5;
padding-top: 4px;
padding-bottom: 4px;
line-height: 1;
color: #888;
}
.cover-fill, .cover-empty {
display:inline-block;
height: 12px;
}
.chart {
line-height: 0;
}
.cover-empty {
background: white;
}
.cover-full {
border-right: none !important;
}
pre.prettyprint {
border: none !important;
padding: 0 !important;
margin: 0 !important;
}
.com { color: #999 !important; }
.ignore-none { color: #999; font-weight: normal; }
.wrapper {
min-height: 100%;
height: auto !important;
height: 100%;
margin: 0 auto -48px;
}
.footer, .push {
height: 48px;
}

View file

@ -0,0 +1,87 @@
/* eslint-disable */
var jumpToCode = (function init() {
// Classes of code we would like to highlight in the file view
var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
// Elements to highlight in the file listing view
var fileListingElements = ['td.pct.low'];
// We don't want to select elements that are direct descendants of another match
var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
// Selecter that finds elements on the page to which we can jump
var selector =
fileListingElements.join(', ') +
', ' +
notSelector +
missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
// The NodeList of matching elements
var missingCoverageElements = document.querySelectorAll(selector);
var currentIndex;
function toggleClass(index) {
missingCoverageElements
.item(currentIndex)
.classList.remove('highlighted');
missingCoverageElements.item(index).classList.add('highlighted');
}
function makeCurrent(index) {
toggleClass(index);
currentIndex = index;
missingCoverageElements.item(index).scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
});
}
function goToPrevious() {
var nextIndex = 0;
if (typeof currentIndex !== 'number' || currentIndex === 0) {
nextIndex = missingCoverageElements.length - 1;
} else if (missingCoverageElements.length > 1) {
nextIndex = currentIndex - 1;
}
makeCurrent(nextIndex);
}
function goToNext() {
var nextIndex = 0;
if (
typeof currentIndex === 'number' &&
currentIndex < missingCoverageElements.length - 1
) {
nextIndex = currentIndex + 1;
}
makeCurrent(nextIndex);
}
return function jump(event) {
if (
document.getElementById('fileSearch') === document.activeElement &&
document.activeElement != null
) {
// if we're currently focused on the search input, we don't want to navigate
return;
}
switch (event.which) {
case 78: // n
case 74: // j
goToNext();
break;
case 66: // b
case 75: // k
case 80: // p
goToPrevious();
break;
}
};
})();
window.addEventListener('keydown', jumpToCode);

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 B

View file

@ -0,0 +1,146 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for All files</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="prettify.css" />
<link rel="stylesheet" href="base.css" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1>All files</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">89.41% </span>
<span class="quiet">Statements</span>
<span class='fraction'>152/170</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">68.18% </span>
<span class="quiet">Branches</span>
<span class='fraction'>30/44</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">85.71% </span>
<span class="quiet">Functions</span>
<span class='fraction'>36/42</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">88.81% </span>
<span class="quiet">Lines</span>
<span class='fraction'>143/161</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input oninput="onInput()" type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line high'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody><tr>
<td class="file high" data-value="ControlSaveTiles.ts"><a href="ControlSaveTiles.ts.html">ControlSaveTiles.ts</a></td>
<td data-value="83.51" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 83%"></div><div class="cover-empty" style="width: 17%"></div></div>
</td>
<td data-value="83.51" class="pct high">83.51%</td>
<td data-value="91" class="abs high">76/91</td>
<td data-value="61.9" class="pct medium">61.9%</td>
<td data-value="21" class="abs medium">13/21</td>
<td data-value="77.27" class="pct medium">77.27%</td>
<td data-value="22" class="abs medium">17/22</td>
<td data-value="82.55" class="pct high">82.55%</td>
<td data-value="86" class="abs high">71/86</td>
</tr>
<tr>
<td class="file high" data-value="TileLayerOffline.ts"><a href="TileLayerOffline.ts.html">TileLayerOffline.ts</a></td>
<td data-value="90.9" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 90%"></div><div class="cover-empty" style="width: 10%"></div></div>
</td>
<td data-value="90.9" class="pct high">90.9%</td>
<td data-value="22" class="abs high">20/22</td>
<td data-value="66.66" class="pct medium">66.66%</td>
<td data-value="12" class="abs medium">8/12</td>
<td data-value="80" class="pct high">80%</td>
<td data-value="5" class="abs high">4/5</td>
<td data-value="90.47" class="pct high">90.47%</td>
<td data-value="21" class="abs high">19/21</td>
</tr>
<tr>
<td class="file high" data-value="TileManager.ts"><a href="TileManager.ts.html">TileManager.ts</a></td>
<td data-value="98.24" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 98%"></div><div class="cover-empty" style="width: 2%"></div></div>
</td>
<td data-value="98.24" class="pct high">98.24%</td>
<td data-value="57" class="abs high">56/57</td>
<td data-value="81.81" class="pct high">81.81%</td>
<td data-value="11" class="abs high">9/11</td>
<td data-value="100" class="pct high">100%</td>
<td data-value="15" class="abs high">15/15</td>
<td data-value="98.14" class="pct high">98.14%</td>
<td data-value="54" class="abs high">53/54</td>
</tr>
</tbody>
</table>
</div>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-02-10T16:19:18.025Z
</div>
<script src="prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="sorter.js"></script>
<script src="block-navigation.js"></script>
</body>
</html>

View file

@ -0,0 +1 @@
.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

View file

@ -0,0 +1,196 @@
/* eslint-disable */
var addSorting = (function() {
'use strict';
var cols,
currentSort = {
index: 0,
desc: false
};
// returns the summary table element
function getTable() {
return document.querySelector('.coverage-summary');
}
// returns the thead element of the summary table
function getTableHeader() {
return getTable().querySelector('thead tr');
}
// returns the tbody element of the summary table
function getTableBody() {
return getTable().querySelector('tbody');
}
// returns the th element for nth column
function getNthColumn(n) {
return getTableHeader().querySelectorAll('th')[n];
}
function onFilterInput() {
const searchValue = document.getElementById('fileSearch').value;
const rows = document.getElementsByTagName('tbody')[0].children;
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
if (
row.textContent
.toLowerCase()
.includes(searchValue.toLowerCase())
) {
row.style.display = '';
} else {
row.style.display = 'none';
}
}
}
// loads the search box
function addSearchBox() {
var template = document.getElementById('filterTemplate');
var templateClone = template.content.cloneNode(true);
templateClone.getElementById('fileSearch').oninput = onFilterInput;
template.parentElement.appendChild(templateClone);
}
// loads all columns
function loadColumns() {
var colNodes = getTableHeader().querySelectorAll('th'),
colNode,
cols = [],
col,
i;
for (i = 0; i < colNodes.length; i += 1) {
colNode = colNodes[i];
col = {
key: colNode.getAttribute('data-col'),
sortable: !colNode.getAttribute('data-nosort'),
type: colNode.getAttribute('data-type') || 'string'
};
cols.push(col);
if (col.sortable) {
col.defaultDescSort = col.type === 'number';
colNode.innerHTML =
colNode.innerHTML + '<span class="sorter"></span>';
}
}
return cols;
}
// attaches a data attribute to every tr element with an object
// of data values keyed by column name
function loadRowData(tableRow) {
var tableCols = tableRow.querySelectorAll('td'),
colNode,
col,
data = {},
i,
val;
for (i = 0; i < tableCols.length; i += 1) {
colNode = tableCols[i];
col = cols[i];
val = colNode.getAttribute('data-value');
if (col.type === 'number') {
val = Number(val);
}
data[col.key] = val;
}
return data;
}
// loads all row data
function loadData() {
var rows = getTableBody().querySelectorAll('tr'),
i;
for (i = 0; i < rows.length; i += 1) {
rows[i].data = loadRowData(rows[i]);
}
}
// sorts the table using the data for the ith column
function sortByIndex(index, desc) {
var key = cols[index].key,
sorter = function(a, b) {
a = a.data[key];
b = b.data[key];
return a < b ? -1 : a > b ? 1 : 0;
},
finalSorter = sorter,
tableBody = document.querySelector('.coverage-summary tbody'),
rowNodes = tableBody.querySelectorAll('tr'),
rows = [],
i;
if (desc) {
finalSorter = function(a, b) {
return -1 * sorter(a, b);
};
}
for (i = 0; i < rowNodes.length; i += 1) {
rows.push(rowNodes[i]);
tableBody.removeChild(rowNodes[i]);
}
rows.sort(finalSorter);
for (i = 0; i < rows.length; i += 1) {
tableBody.appendChild(rows[i]);
}
}
// removes sort indicators for current column being sorted
function removeSortIndicators() {
var col = getNthColumn(currentSort.index),
cls = col.className;
cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
col.className = cls;
}
// adds sort indicators for current column being sorted
function addSortIndicators() {
getNthColumn(currentSort.index).className += currentSort.desc
? ' sorted-desc'
: ' sorted';
}
// adds event listeners for all sorter widgets
function enableUI() {
var i,
el,
ithSorter = function ithSorter(i) {
var col = cols[i];
return function() {
var desc = col.defaultDescSort;
if (currentSort.index === i) {
desc = !currentSort.desc;
}
sortByIndex(i, desc);
removeSortIndicators();
currentSort.index = i;
currentSort.desc = desc;
addSortIndicators();
};
};
for (i = 0; i < cols.length; i += 1) {
if (cols[i].sortable) {
// add the click event handler on the th so users
// dont have to click on those tiny arrows
el = getNthColumn(i).querySelector('.sorter').parentElement;
if (el.addEventListener) {
el.addEventListener('click', ithSorter(i));
} else {
el.attachEvent('onclick', ithSorter(i));
}
}
}
}
// adds sorting functionality to the UI
return function() {
if (!getTable()) {
return;
}
cols = loadColumns();
loadData();
addSearchBox();
addSortIndicators();
enableUI();
};
})();
window.addEventListener('load', addSorting);

View file

@ -0,0 +1,316 @@
TN:
SF:src/ControlSaveTiles.ts
FN:60,(anonymous_0)
FN:82,(anonymous_1)
FN:87,(anonymous_2)
FN:92,(anonymous_3)
FN:95,(anonymous_4)
FN:96,(anonymous_5)
FN:103,(anonymous_6)
FN:107,(anonymous_7)
FN:120,(anonymous_8)
FN:139,(anonymous_9)
FN:142,(anonymous_10)
FN:142,(anonymous_11)
FN:144,(anonymous_12)
FN:144,(anonymous_13)
FN:167,(anonymous_14)
FN:202,(anonymous_15)
FN:212,(anonymous_16)
FN:230,(anonymous_18)
FN:240,(anonymous_20)
FN:241,(anonymous_21)
FN:242,(anonymous_22)
FN:256,savetiles
FNF:22
FNH:17
FNDA:9,(anonymous_0)
FNDA:13,(anonymous_1)
FNDA:13,(anonymous_2)
FNDA:0,(anonymous_3)
FNDA:0,(anonymous_4)
FNDA:0,(anonymous_5)
FNDA:0,(anonymous_6)
FNDA:10,(anonymous_7)
FNDA:20,(anonymous_8)
FNDA:6,(anonymous_9)
FNDA:5,(anonymous_10)
FNDA:5,(anonymous_11)
FNDA:18,(anonymous_12)
FNDA:18,(anonymous_13)
FNDA:6,(anonymous_14)
FNDA:5,(anonymous_15)
FNDA:0,(anonymous_16)
FNDA:8,(anonymous_18)
FNDA:9,(anonymous_20)
FNDA:9,(anonymous_21)
FNDA:9,(anonymous_22)
FNDA:9,savetiles
DA:52,9
DA:61,9
DA:62,9
DA:63,9
DA:64,9
DA:83,13
DA:84,0
DA:86,13
DA:88,13
DA:89,13
DA:90,13
DA:92,0
DA:96,0
DA:97,0
DA:98,0
DA:104,0
DA:108,10
DA:109,10
DA:110,10
DA:116,10
DA:117,10
DA:126,20
DA:127,20
DA:128,20
DA:129,20
DA:131,20
DA:136,20
DA:140,6
DA:141,6
DA:142,6
DA:143,5
DA:144,18
DA:145,18
DA:146,18
DA:147,9
DA:149,9
DA:150,9
DA:151,8
DA:153,9
DA:155,5
DA:156,5
DA:157,9
DA:160,6
DA:161,1
DA:163,5
DA:168,6
DA:170,6
DA:172,6
DA:174,6
DA:175,1
DA:176,1
DA:177,0
DA:181,1
DA:183,1
DA:184,4
DA:187,5
DA:190,6
DA:192,6
DA:193,10
DA:197,10
DA:199,6
DA:203,5
DA:214,0
DA:218,0
DA:221,0
DA:223,0
DA:224,0
DA:225,0
DA:227,0
DA:231,8
DA:232,8
DA:233,8
DA:234,8
DA:235,3
DA:236,3
DA:241,9
DA:242,9
DA:243,9
DA:244,9
DA:245,9
DA:248,9
DA:249,0
DA:251,9
DA:260,9
DA:264,1
DA:266,1
LF:86
LH:71
BRDA:83,0,0,0
BRDA:97,1,0,0
BRDA:146,2,0,9
BRDA:150,3,0,8
BRDA:160,4,0,1
BRDA:160,4,1,5
BRDA:174,5,0,1
BRDA:174,5,1,5
BRDA:176,6,0,0
BRDA:187,7,0,5
BRDA:187,7,1,4
BRDA:190,8,0,6
BRDA:190,8,1,6
BRDA:214,9,0,0
BRDA:215,10,0,0
BRDA:215,10,1,0
BRDA:224,11,0,0
BRDA:234,12,0,3
BRDA:248,13,0,0
BRDA:248,13,1,9
BRDA:264,14,0,1
BRF:21
BRH:13
end_of_record
TN:
SF:src/TileLayerOffline.ts
FN:20,(anonymous_0)
FN:38,(anonymous_1)
FN:49,(anonymous_2)
FN:61,(anonymous_3)
FN:93,tileLayerOffline
FNF:5
FNH:4
FNDA:10,(anonymous_0)
FNDA:10,(anonymous_1)
FNDA:13,(anonymous_2)
FNDA:15,(anonymous_3)
FNDA:0,tileLayerOffline
DA:21,10
DA:23,10
DA:24,10
DA:26,10
DA:27,0
DA:31,10
DA:33,10
DA:35,10
DA:38,10
DA:40,10
DA:50,13
DA:62,15
DA:63,15
DA:64,15
DA:65,90
DA:66,90
DA:72,90
DA:89,15
DA:94,0
DA:98,2
DA:100,2
LF:21
LH:19
BRDA:26,0,0,0
BRDA:26,1,0,10
BRDA:26,1,1,10
BRDA:28,2,0,0
BRDA:28,2,1,0
BRDA:70,3,0,90
BRDA:70,3,1,90
BRDA:75,4,0,0
BRDA:75,4,1,90
BRDA:75,5,0,90
BRDA:75,5,1,90
BRDA:98,6,0,2
BRF:12
BRH:8
end_of_record
TN:
SF:src/TileManager.ts
FN:29,openTilesDataBase
FN:34,(anonymous_1)
FN:57,getStorageLength
FN:69,getStorageInfo
FN:84,downloadTile
FN:97,saveTile
FN:108,getTileUrl
FN:115,getTilePoints
FN:145,getStoredTilesAsJson
FN:192,removeTile
FN:200,getBlobByKey
FN:203,(anonymous_17)
FN:206,hasTile
FN:215,truncate
FN:219,getTileImageSource
FNF:15
FNH:15
FNDA:69,openTilesDataBase
FNDA:1,(anonymous_1)
FNDA:14,getStorageLength
FNDA:3,getStorageInfo
FNDA:2,downloadTile
FNDA:14,saveTile
FNDA:193,getTileUrl
FNDA:16,getTilePoints
FNDA:1,getStoredTilesAsJson
FNDA:1,removeTile
FNDA:1,getBlobByKey
FNDA:1,(anonymous_17)
FNDA:14,hasTile
FNDA:22,truncate
FNDA:12,getTileImageSource
DA:25,3
DA:26,3
DA:30,69
DA:31,66
DA:33,3
DA:35,1
DA:36,1
DA:38,1
DA:39,1
DA:42,1
DA:43,1
DA:47,3
DA:58,14
DA:59,14
DA:72,3
DA:73,3
DA:74,3
DA:85,2
DA:86,2
DA:87,1
DA:89,1
DA:101,14
DA:102,14
DA:109,193
DA:116,16
DA:117,16
DA:118,0
DA:120,16
DA:121,16
DA:123,16
DA:124,31
DA:125,91
DA:128,16
DA:149,1
DA:153,1
DA:154,1
DA:158,1
DA:163,1
DA:164,1
DA:168,1
DA:186,1
DA:193,1
DA:194,1
DA:201,1
DA:203,1
DA:207,14
DA:208,14
DA:209,14
DA:216,22
DA:220,12
DA:221,12
DA:222,11
DA:224,1
DA:225,1
LF:54
LH:53
BRDA:30,0,0,66
BRDA:38,1,0,1
BRDA:86,2,0,1
BRDA:111,3,0,0
BRDA:111,3,1,193
BRDA:117,4,0,0
BRDA:117,5,0,16
BRDA:117,5,1,16
BRDA:203,6,0,1
BRDA:203,6,1,1
BRDA:221,7,0,11
BRF:11
BRH:9
end_of_record

View file

@ -0,0 +1 @@
.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

View file

@ -0,0 +1,196 @@
/* eslint-disable */
var addSorting = (function() {
'use strict';
var cols,
currentSort = {
index: 0,
desc: false
};
// returns the summary table element
function getTable() {
return document.querySelector('.coverage-summary');
}
// returns the thead element of the summary table
function getTableHeader() {
return getTable().querySelector('thead tr');
}
// returns the tbody element of the summary table
function getTableBody() {
return getTable().querySelector('tbody');
}
// returns the th element for nth column
function getNthColumn(n) {
return getTableHeader().querySelectorAll('th')[n];
}
function onFilterInput() {
const searchValue = document.getElementById('fileSearch').value;
const rows = document.getElementsByTagName('tbody')[0].children;
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
if (
row.textContent
.toLowerCase()
.includes(searchValue.toLowerCase())
) {
row.style.display = '';
} else {
row.style.display = 'none';
}
}
}
// loads the search box
function addSearchBox() {
var template = document.getElementById('filterTemplate');
var templateClone = template.content.cloneNode(true);
templateClone.getElementById('fileSearch').oninput = onFilterInput;
template.parentElement.appendChild(templateClone);
}
// loads all columns
function loadColumns() {
var colNodes = getTableHeader().querySelectorAll('th'),
colNode,
cols = [],
col,
i;
for (i = 0; i < colNodes.length; i += 1) {
colNode = colNodes[i];
col = {
key: colNode.getAttribute('data-col'),
sortable: !colNode.getAttribute('data-nosort'),
type: colNode.getAttribute('data-type') || 'string'
};
cols.push(col);
if (col.sortable) {
col.defaultDescSort = col.type === 'number';
colNode.innerHTML =
colNode.innerHTML + '<span class="sorter"></span>';
}
}
return cols;
}
// attaches a data attribute to every tr element with an object
// of data values keyed by column name
function loadRowData(tableRow) {
var tableCols = tableRow.querySelectorAll('td'),
colNode,
col,
data = {},
i,
val;
for (i = 0; i < tableCols.length; i += 1) {
colNode = tableCols[i];
col = cols[i];
val = colNode.getAttribute('data-value');
if (col.type === 'number') {
val = Number(val);
}
data[col.key] = val;
}
return data;
}
// loads all row data
function loadData() {
var rows = getTableBody().querySelectorAll('tr'),
i;
for (i = 0; i < rows.length; i += 1) {
rows[i].data = loadRowData(rows[i]);
}
}
// sorts the table using the data for the ith column
function sortByIndex(index, desc) {
var key = cols[index].key,
sorter = function(a, b) {
a = a.data[key];
b = b.data[key];
return a < b ? -1 : a > b ? 1 : 0;
},
finalSorter = sorter,
tableBody = document.querySelector('.coverage-summary tbody'),
rowNodes = tableBody.querySelectorAll('tr'),
rows = [],
i;
if (desc) {
finalSorter = function(a, b) {
return -1 * sorter(a, b);
};
}
for (i = 0; i < rowNodes.length; i += 1) {
rows.push(rowNodes[i]);
tableBody.removeChild(rowNodes[i]);
}
rows.sort(finalSorter);
for (i = 0; i < rows.length; i += 1) {
tableBody.appendChild(rows[i]);
}
}
// removes sort indicators for current column being sorted
function removeSortIndicators() {
var col = getNthColumn(currentSort.index),
cls = col.className;
cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
col.className = cls;
}
// adds sort indicators for current column being sorted
function addSortIndicators() {
getNthColumn(currentSort.index).className += currentSort.desc
? ' sorted-desc'
: ' sorted';
}
// adds event listeners for all sorter widgets
function enableUI() {
var i,
el,
ithSorter = function ithSorter(i) {
var col = cols[i];
return function() {
var desc = col.defaultDescSort;
if (currentSort.index === i) {
desc = !currentSort.desc;
}
sortByIndex(i, desc);
removeSortIndicators();
currentSort.index = i;
currentSort.desc = desc;
addSortIndicators();
};
};
for (i = 0; i < cols.length; i += 1) {
if (cols[i].sortable) {
// add the click event handler on the th so users
// dont have to click on those tiny arrows
el = getNthColumn(i).querySelector('.sorter').parentElement;
if (el.addEventListener) {
el.addEventListener('click', ithSorter(i));
} else {
el.attachEvent('onclick', ithSorter(i));
}
}
}
}
// adds sorting functionality to the UI
return function() {
if (!getTable()) {
return;
}
cols = loadColumns();
loadData();
addSearchBox();
addSortIndicators();
enableUI();
};
})();
window.addEventListener('load', addSorting);

478
pwa/node_modules/leaflet.offline/dist/bundle.js generated vendored Normal file
View file

@ -0,0 +1,478 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('leaflet'), require('idb')) :
typeof define === 'function' && define.amd ? define(['exports', 'leaflet', 'idb'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.LeafletOffline = {}, global.L, global.idb));
})(this, (function (exports, leaflet, idb) { 'use strict';
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, [])).next());
});
}
/**
* Api methods used in control and layer
* For advanced usage
*
* @module TileManager
*
*/
const tileStoreName = 'tileStore';
const urlTemplateIndex = 'urlTemplate';
let dbPromise;
function openTilesDataBase() {
if (dbPromise) {
return dbPromise;
}
dbPromise = idb.openDB('leaflet.offline', 2, {
upgrade(db, oldVersion) {
idb.deleteDB('leaflet_offline');
idb.deleteDB('leaflet_offline_areas');
if (oldVersion < 1) {
const tileStore = db.createObjectStore(tileStoreName, {
keyPath: 'key',
});
tileStore.createIndex(urlTemplateIndex, 'urlTemplate');
tileStore.createIndex('z', 'z');
}
},
});
return dbPromise;
}
/**
* @example
* ```js
* import { getStorageLength } from 'leaflet.offline'
* getStorageLength().then(i => console.log(i + 'tiles in storage'))
* ```
*/
function getStorageLength() {
return __awaiter(this, void 0, void 0, function* () {
const db = yield openTilesDataBase();
return db.count(tileStoreName);
});
}
/**
* @example
* ```js
* import { getStorageInfo } from 'leaflet.offline'
* getStorageInfo('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png')
* ```
*/
function getStorageInfo(urlTemplate) {
return __awaiter(this, void 0, void 0, function* () {
const range = IDBKeyRange.only(urlTemplate);
const db = yield openTilesDataBase();
return db.getAllFromIndex(tileStoreName, urlTemplateIndex, range);
});
}
/**
* @example
* ```js
* import { downloadTile } from 'leaflet.offline'
* downloadTile(tileInfo.url).then(blob => saveTile(tileInfo, blob))
* ```
*/
function downloadTile(tileUrl) {
return __awaiter(this, void 0, void 0, function* () {
const response = yield fetch(tileUrl);
if (!response.ok) {
throw new Error(`Request failed with status ${response.statusText}`);
}
return response.blob();
});
}
/**
* @example
* ```js
* saveTile(tileInfo, blob).then(() => console.log(`saved tile from ${tileInfo.url}`))
* ```
*/
function saveTile(tileInfo, blob) {
return __awaiter(this, void 0, void 0, function* () {
const db = yield openTilesDataBase();
return db.put(tileStoreName, Object.assign({ blob }, tileInfo));
});
}
function getTileUrl(urlTemplate, data) {
return leaflet.Util.template(urlTemplate, Object.assign(Object.assign({}, data), { r: leaflet.Browser.retina ? '@2x' : '' }));
}
function getTilePoints(area, tileSize) {
const points = [];
if (!area.min || !area.max) {
return points;
}
const topLeftTile = area.min.divideBy(tileSize.x).floor();
const bottomRightTile = area.max.divideBy(tileSize.x).floor();
for (let j = topLeftTile.y; j <= bottomRightTile.y; j += 1) {
for (let i = topLeftTile.x; i <= bottomRightTile.x; i += 1) {
points.push(new leaflet.Point(i, j));
}
}
return points;
}
/**
* Get a geojson of tiles from one resource
*
* @example
* const urlTemplate = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
* const getGeoJsonData = () => LeafletOffline.getStorageInfo(urlTemplate)
* .then((data) => LeafletOffline.getStoredTilesAsJson(baseLayer, data));
*
* getGeoJsonData().then((geojson) => {
* storageLayer = L.geoJSON(geojson).bindPopup(
* (clickedLayer) => clickedLayer.feature.properties.key,
* );
* });
*
*/
function getStoredTilesAsJson(tileSize, tiles) {
const featureCollection = {
type: 'FeatureCollection',
features: [],
};
for (let i = 0; i < tiles.length; i += 1) {
const topLeftPoint = new leaflet.Point(tiles[i].x * tileSize.x, tiles[i].y * tileSize.y);
const bottomRightPoint = new leaflet.Point(topLeftPoint.x + tileSize.x, topLeftPoint.y + tileSize.y);
const topLeftlatlng = leaflet.CRS.EPSG3857.pointToLatLng(topLeftPoint, tiles[i].z);
const botRightlatlng = leaflet.CRS.EPSG3857.pointToLatLng(bottomRightPoint, tiles[i].z);
featureCollection.features.push({
type: 'Feature',
properties: tiles[i],
geometry: {
type: 'Polygon',
coordinates: [
[
[topLeftlatlng.lng, topLeftlatlng.lat],
[botRightlatlng.lng, topLeftlatlng.lat],
[botRightlatlng.lng, botRightlatlng.lat],
[topLeftlatlng.lng, botRightlatlng.lat],
[topLeftlatlng.lng, topLeftlatlng.lat],
],
],
},
});
}
return featureCollection;
}
/**
* Remove tile by key
*/
function removeTile(key) {
return __awaiter(this, void 0, void 0, function* () {
const db = yield openTilesDataBase();
return db.delete(tileStoreName, key);
});
}
/**
* Get single tile blob
*/
function getBlobByKey(key) {
return __awaiter(this, void 0, void 0, function* () {
return (yield openTilesDataBase())
.get(tileStoreName, key)
.then((result) => result && result.blob);
});
}
function hasTile(key) {
return __awaiter(this, void 0, void 0, function* () {
const db = yield openTilesDataBase();
const result = yield db.getKey(tileStoreName, key);
return result !== undefined;
});
}
/**
* Remove everything
*/
function truncate() {
return __awaiter(this, void 0, void 0, function* () {
return (yield openTilesDataBase()).clear(tileStoreName);
});
}
function getTileImageSource(key, url) {
return __awaiter(this, void 0, void 0, function* () {
const shouldUseUrl = !(yield hasTile(key));
if (shouldUseUrl) {
return url;
}
const blob = yield getBlobByKey(key);
return URL.createObjectURL(blob);
});
}
class TileLayerOffline extends leaflet.TileLayer {
createTile(coords, done) {
const tile = document.createElement('img');
leaflet.DomEvent.on(tile, 'load', leaflet.Util.bind(this._tileOnLoad, this, done, tile));
leaflet.DomEvent.on(tile, 'error', leaflet.Util.bind(this._tileOnError, this, done, tile));
if (this.options.crossOrigin || this.options.crossOrigin === '') {
tile.crossOrigin =
this.options.crossOrigin === true ? '' : this.options.crossOrigin;
}
tile.alt = '';
tile.setAttribute('role', 'presentation');
getTileImageSource(this._getStorageKey(coords), this.getTileUrl(coords)).then((src) => (tile.src = src));
return tile;
}
/**
* get key to use for storage
* @private
* @param {string} url url used to load tile
* @return {string} unique identifier.
*/
_getStorageKey(coords) {
return getTileUrl(this._url, Object.assign(Object.assign(Object.assign({}, coords), this.options), {
// @ts-ignore: Possibly undefined
s: this.options.subdomains['0'] }));
}
/**
* Get tileinfo for zoomlevel & bounds
*/
getTileUrls(bounds, zoom) {
var _a;
const tiles = [];
const tilePoints = getTilePoints(bounds, this.getTileSize());
for (let index = 0; index < tilePoints.length; index += 1) {
const tilePoint = tilePoints[index];
const data = Object.assign(Object.assign({}, this.options), { x: tilePoint.x, y: tilePoint.y, z: zoom + (this.options.zoomOffset || 0) });
tiles.push({
key: getTileUrl(this._url, Object.assign(Object.assign({}, data), { s: (_a = this.options.subdomains) === null || _a === void 0 ? void 0 : _a[0] })),
url: getTileUrl(this._url, Object.assign(Object.assign({}, data), {
// @ts-ignore: Undefined
s: this._getSubdomain(tilePoint) })),
z: zoom,
x: tilePoint.x,
y: tilePoint.y,
urlTemplate: this._url,
createdAt: Date.now(),
});
}
return tiles;
}
}
function tileLayerOffline(url, options) {
return new TileLayerOffline(url, options);
}
/** @ts-ignore */
if (window.L) {
/** @ts-ignore */
window.L.tileLayer.offline = tileLayerOffline;
}
class ControlSaveTiles extends leaflet.Control {
constructor(baseLayer, options) {
super(options);
this.status = {
storagesize: 0,
lengthToBeSaved: 0,
lengthSaved: 0,
lengthLoaded: 0,
_tilesforSave: [],
};
this._baseLayer = baseLayer;
this.setStorageSize();
this.options = Object.assign({
position: 'topleft',
saveText: '+',
rmText: '-',
maxZoom: 19,
saveWhatYouSee: false,
bounds: null,
confirm: null,
confirmRemoval: null,
parallel: 50,
zoomlevels: undefined,
alwaysDownload: true,
}, options);
}
setStorageSize() {
if (this.status.storagesize) {
return Promise.resolve(this.status.storagesize);
}
return getStorageLength()
.then((numberOfKeys) => {
this.status.storagesize = numberOfKeys;
this._baseLayer.fire('storagesize', this.status);
return numberOfKeys;
})
.catch(() => 0);
}
getStorageSize(callback) {
this.setStorageSize().then((result) => {
if (callback) {
callback(result);
}
});
}
setLayer(layer) {
this._baseLayer = layer;
}
onAdd() {
const container = leaflet.DomUtil.create('div', 'savetiles leaflet-bar');
const { options } = this;
this._createButton(options.saveText, 'savetiles', container, this._saveTiles);
this._createButton(options.rmText, 'rmtiles', container, this._rmTiles);
return container;
}
_createButton(html, className, container, fn) {
const link = leaflet.DomUtil.create('a', className, container);
link.innerHTML = html;
link.href = '#';
link.ariaRoleDescription = 'button';
leaflet.DomEvent.on(link, 'mousedown dblclick', leaflet.DomEvent.stopPropagation)
.on(link, 'click', leaflet.DomEvent.stop)
.on(link, 'click', fn, this)
.on(link, 'click', this._refocusOnMap, this);
return link;
}
_saveTiles() {
const tiles = this._calculateTiles();
this._resetStatus(tiles);
const successCallback = () => __awaiter(this, void 0, void 0, function* () {
this._baseLayer.fire('savestart', this.status);
const loader = () => __awaiter(this, void 0, void 0, function* () {
const tile = tiles.shift();
if (tile === undefined) {
return Promise.resolve();
}
const blob = yield this._loadTile(tile);
if (blob) {
yield this._saveTile(tile, blob);
}
return loader();
});
const parallel = Math.min(tiles.length, this.options.parallel);
for (let i = 0; i < parallel; i += 1) {
loader();
}
});
if (this.options.confirm) {
this.options.confirm(this.status, successCallback);
}
else {
successCallback();
}
}
_calculateTiles() {
let tiles = [];
// minimum zoom to prevent the user from saving the whole world
const minZoom = 5;
// current zoom or zoom options
let zoomlevels = [];
if (this.options.saveWhatYouSee) {
const currentZoom = this._map.getZoom();
if (currentZoom < minZoom) {
throw new Error(`It's not possible to save with zoom below level ${minZoom}.`);
}
const { maxZoom } = this.options;
for (let zoom = currentZoom; zoom <= maxZoom; zoom += 1) {
zoomlevels.push(zoom);
}
}
else {
zoomlevels = this.options.zoomlevels || [this._map.getZoom()];
}
const latlngBounds = this.options.bounds || this._map.getBounds();
for (let i = 0; i < zoomlevels.length; i += 1) {
const area = leaflet.bounds(this._map.project(latlngBounds.getNorthWest(), zoomlevels[i]), this._map.project(latlngBounds.getSouthEast(), zoomlevels[i]));
tiles = tiles.concat(this._baseLayer.getTileUrls(area, zoomlevels[i]));
}
return tiles;
}
_resetStatus(tiles) {
this.status = {
lengthLoaded: 0,
lengthToBeSaved: tiles.length,
lengthSaved: 0,
_tilesforSave: tiles,
storagesize: this.status.storagesize,
};
}
_loadTile(tile) {
return __awaiter(this, void 0, void 0, function* () {
let blob;
if (this.options.alwaysDownload === true ||
(yield hasTile(tile.key)) === false) {
blob = yield downloadTile(tile.url);
}
this.status.lengthLoaded += 1;
this._baseLayer.fire('loadtileend', this.status);
if (this.status.lengthLoaded === this.status.lengthToBeSaved) {
this._baseLayer.fire('loadend', this.status);
}
return blob;
});
}
_saveTile(tile, blob) {
return __awaiter(this, void 0, void 0, function* () {
yield saveTile(tile, blob);
this.status.lengthSaved += 1;
this._baseLayer.fire('savetileend', this.status);
if (this.status.lengthSaved === this.status.lengthToBeSaved) {
this._baseLayer.fire('saveend', this.status);
this.setStorageSize();
}
});
}
_rmTiles() {
const successCallback = () => {
truncate().then(() => {
this.status.storagesize = 0;
this._baseLayer.fire('tilesremoved');
this._baseLayer.fire('storagesize', this.status);
});
};
if (this.options.confirmRemoval) {
this.options.confirmRemoval(this.status, successCallback);
}
else {
successCallback();
}
}
}
function savetiles(baseLayer, options) {
return new ControlSaveTiles(baseLayer, options);
}
/** @ts-ignore */
if (window.L) {
/** @ts-ignore */
window.L.control.savetiles = savetiles;
}
exports.downloadTile = downloadTile;
exports.getBlobByKey = getBlobByKey;
exports.getStorageInfo = getStorageInfo;
exports.getStorageLength = getStorageLength;
exports.getStoredTilesAsJson = getStoredTilesAsJson;
exports.getTileImageSource = getTileImageSource;
exports.getTilePoints = getTilePoints;
exports.getTileUrl = getTileUrl;
exports.hasTile = hasTile;
exports.removeTile = removeTile;
exports.saveTile = saveTile;
exports.savetiles = savetiles;
exports.tileLayerOffline = tileLayerOffline;
exports.truncate = truncate;
}));

View file

@ -0,0 +1,42 @@
import { Control, ControlOptions, DomEvent, LatLngBounds, Map } from 'leaflet';
import { TileLayerOffline } from './TileLayerOffline';
import { TileInfo } from './TileManager';
export interface SaveTileOptions extends ControlOptions {
saveText: string;
rmText: string;
maxZoom: number;
saveWhatYouSee: boolean;
bounds: LatLngBounds | null;
confirm: ((status: SaveStatus, successCallback: Function) => void) | null;
confirmRemoval: ((status: SaveStatus, successCallback: Function) => void) | null;
parallel: number;
zoomlevels?: number[];
alwaysDownload: boolean;
}
export interface SaveStatus {
_tilesforSave: TileInfo[];
storagesize: number;
lengthToBeSaved: number;
lengthSaved: number;
lengthLoaded: number;
}
export declare class ControlSaveTiles extends Control {
_map: Map;
_refocusOnMap: DomEvent.EventHandlerFn;
_baseLayer: TileLayerOffline;
options: SaveTileOptions;
status: SaveStatus;
constructor(baseLayer: TileLayerOffline, options: Partial<SaveTileOptions>);
setStorageSize(): Promise<number>;
getStorageSize(callback: Function): void;
setLayer(layer: TileLayerOffline): void;
onAdd(): HTMLDivElement;
_createButton(html: string, className: string, container: HTMLElement, fn: DomEvent.EventHandlerFn): HTMLAnchorElement;
_saveTiles(): void;
_calculateTiles(): TileInfo[];
_resetStatus(tiles: TileInfo[]): void;
_loadTile(tile: TileInfo): Promise<Blob | undefined>;
_saveTile(tile: TileInfo, blob: Blob): Promise<void>;
_rmTiles(): void;
}
export declare function savetiles(baseLayer: TileLayerOffline, options: Partial<SaveTileOptions>): ControlSaveTiles;

View file

@ -0,0 +1,22 @@
import { Bounds, Coords, DoneCallback, TileLayer, TileLayerOptions } from 'leaflet';
import { TileInfo } from './TileManager';
export declare class TileLayerOffline extends TileLayer {
_url: string;
createTile(coords: Coords, done: DoneCallback): HTMLElement;
/**
* get key to use for storage
* @private
* @param {string} url url used to load tile
* @return {string} unique identifier.
*/
_getStorageKey(coords: {
x: number;
y: number;
z: number;
}): string;
/**
* Get tileinfo for zoomlevel & bounds
*/
getTileUrls(bounds: Bounds, zoom: number): TileInfo[];
}
export declare function tileLayerOffline(url: string, options: TileLayerOptions): TileLayerOffline;

View file

@ -0,0 +1,89 @@
/**
* Api methods used in control and layer
* For advanced usage
*
* @module TileManager
*
*/
import { Bounds, Point } from 'leaflet';
import { IDBPDatabase } from 'idb';
import { FeatureCollection, Polygon } from 'geojson';
export type TileInfo = {
key: string;
url: string;
urlTemplate: string;
x: number;
y: number;
z: number;
createdAt: number;
};
export type StoredTile = TileInfo & {
blob: Blob;
};
export declare function openTilesDataBase(): Promise<IDBPDatabase>;
/**
* @example
* ```js
* import { getStorageLength } from 'leaflet.offline'
* getStorageLength().then(i => console.log(i + 'tiles in storage'))
* ```
*/
export declare function getStorageLength(): Promise<number>;
/**
* @example
* ```js
* import { getStorageInfo } from 'leaflet.offline'
* getStorageInfo('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png')
* ```
*/
export declare function getStorageInfo(urlTemplate: string): Promise<StoredTile[]>;
/**
* @example
* ```js
* import { downloadTile } from 'leaflet.offline'
* downloadTile(tileInfo.url).then(blob => saveTile(tileInfo, blob))
* ```
*/
export declare function downloadTile(tileUrl: string): Promise<Blob>;
/**
* @example
* ```js
* saveTile(tileInfo, blob).then(() => console.log(`saved tile from ${tileInfo.url}`))
* ```
*/
export declare function saveTile(tileInfo: TileInfo, blob: Blob): Promise<IDBValidKey>;
export declare function getTileUrl(urlTemplate: string, data: any): string;
export declare function getTilePoints(area: Bounds, tileSize: Point): Point[];
/**
* Get a geojson of tiles from one resource
*
* @example
* const urlTemplate = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
* const getGeoJsonData = () => LeafletOffline.getStorageInfo(urlTemplate)
* .then((data) => LeafletOffline.getStoredTilesAsJson(baseLayer, data));
*
* getGeoJsonData().then((geojson) => {
* storageLayer = L.geoJSON(geojson).bindPopup(
* (clickedLayer) => clickedLayer.feature.properties.key,
* );
* });
*
*/
export declare function getStoredTilesAsJson(tileSize: {
x: number;
y: number;
}, tiles: TileInfo[]): FeatureCollection<Polygon>;
/**
* Remove tile by key
*/
export declare function removeTile(key: string): Promise<void>;
/**
* Get single tile blob
*/
export declare function getBlobByKey(key: string): Promise<Blob>;
export declare function hasTile(key: string): Promise<boolean>;
/**
* Remove everything
*/
export declare function truncate(): Promise<void>;
export declare function getTileImageSource(key: string, url: string): Promise<string>;

View file

@ -0,0 +1,6 @@
export { tileLayerOffline } from './TileLayerOffline';
export { savetiles } from './ControlSaveTiles';
export type { SaveStatus, ControlSaveTiles, SaveTileOptions, } from './ControlSaveTiles';
export type { TileInfo, StoredTile } from './TileManager';
export type { TileLayerOffline } from './TileLayerOffline';
export { getStorageInfo, getStorageLength, getStoredTilesAsJson, removeTile, truncate, downloadTile, saveTile, hasTile, getBlobByKey, getTilePoints, getTileUrl, getTileImageSource, } from './TileManager';

View file

@ -0,0 +1 @@
export {};

View file

@ -0,0 +1 @@
export {};

View file

@ -0,0 +1 @@
export {};

10
pwa/node_modules/leaflet.offline/examples/README.md generated vendored Normal file
View file

@ -0,0 +1,10 @@
# Examples
The examples are based on [vite's vanilla-ts template](https://vite.dev/guide/#scaffolding-your-first-vite-project)
```
cd dir/
npm i
npm run dev
```

View file

@ -0,0 +1,60 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Storage</title>
</head>
<body>
<div class="container-fluid py-md-3">
<main id="app">
<h1>List files in storage</h1>
<div class="form-row">
<div class="form-group col-md-6">
<label for="selectlayer">Select layer:</label>
<select class="form-control" id="selectlayer"></select>
</div>
</div>
<p>
Total <span id="storage"></span>
<button class="btn btn-danger" id="remove_tiles">
<i
class="fa fa-trash"
aria-hidden="true"
title="Remove all tiles from all layers"
></i>
All
</button>
<button class="btn btn-warning" id="remove_old_tiles">
<i
class="fa fa-archive"
aria-hidden="true"
title="Remove old osm tiles"
></i>
Old
</button>
</p>
<table class="table table-striped table-sm table-fixed">
<colgroup>
<col style="width: 50px" />
<col />
<col />
<col style="width: 100px" />
</colgroup>
<thead>
<tr>
<th></th>
<th>Source url</th>
<th>Storage key</th>
<th>Bytes</th>
</tr>
</thead>
<tbody id="tileinforows"></tbody>
</table>
</main>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,22 @@
{
"name": "list-storage",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"idb": "^8.0.0",
"leaflet": "^1.9.4",
"leaflet.offline": "^3.1.0",
"prettier": "^3.3.3",
"typescript": "~5.6.2",
"vite": "^6.3.5"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^7.1.0"
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,70 @@
import './style.css';
import { getStorageInfo, removeTile, truncate } from 'leaflet.offline';
export const urlTemplate = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
export const wmtsUrlTemplate =
'https://service.pdok.nl/brt/achtergrondkaart/wmts/v2_0?service=WMTS&request=GetTile&version=1.0.0&tilematrixset=EPSG:3857&layer={layer}&tilematrix={z}&tilerow={y}&tilecol={x}&format=image%2Fpng';
const layerSelector = document.getElementById(
'selectlayer',
) as HTMLSelectElement;
function createLayerOpts() {
const xyzOpt = document.createElement('option');
xyzOpt.value = urlTemplate;
xyzOpt.text = 'osm';
const wmtsOpt = document.createElement('option');
wmtsOpt.value = wmtsUrlTemplate;
wmtsOpt.text = 'wmts api layer';
layerSelector.add(xyzOpt);
layerSelector.add(wmtsOpt);
layerSelector.value = urlTemplate;
}
const createTable = (url: string) =>
getStorageInfo(url).then((r) => {
document.getElementById('storage')!.innerHTML = r.length.toString();
const list = document.getElementById(
'tileinforows',
) as HTMLTableSectionElement;
list.innerHTML = '';
for (let i = 0; i < r.length; i += 1) {
const tileInfo = r[i];
list.insertAdjacentHTML(
'beforeend',
`<tr>
<td>${i}</td>
<td class="text-truncate">${tileInfo.url}</td>
<td class="text-truncate">${r[i].key}</td>
<td>${tileInfo.blob.size}</td>
</tr>`,
);
}
});
createLayerOpts();
createTable(layerSelector.value);
const rmButton = document.getElementById('remove_tiles') as HTMLButtonElement;
rmButton.addEventListener('click', () =>
truncate().then(() => createTable(layerSelector.value)),
);
const rmOldButton = document.getElementById(
'remove_old_tiles',
) as HTMLButtonElement;
rmOldButton.addEventListener('click', async () => {
const tiles = await getStorageInfo(urlTemplate);
const minCreatedAt = new Date().setDate(-30);
await Promise.all(
tiles.map((tile) =>
tile.createdAt < minCreatedAt ? removeTile(tile.key) : Promise.resolve(),
),
);
createTable(layerSelector.value);
});
layerSelector.addEventListener('change', (e) => {
// @ts-ignore
createTable(e.target.value);
});

View file

@ -0,0 +1,40 @@
@import 'leaflet/dist/leaflet.css';
@import '/node_modules/@fortawesome/fontawesome-free/css/all.min.css';
body {
margin: 0;
font-family: sans-serif;
}
#app {
height: 100vh;
max-width: 1200px;
margin: 0 auto;
}
.btn-danger {
--background-color: red;
--foreground-color: white;
}
.btn-warning {
--background-color: orange;
--foreground-color: white;
}
.btn {
border: var(--background-color) 1px solid;
background-color: var(--background-color);
color: var(--foreground-color);
padding: 0.5em;
}
.table-striped {
td {
padding: 0.5em;
}
tr:nth-child(even) {
background-color: #f2f2f2;
}
}

View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

View file

@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View file

@ -0,0 +1,51 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + TS</title>
</head>
<body>
<div class="container">
<main id="app">
<h1>Map with layers & control</h1>
<p>
This maps shows the default
<a
href="https://github.com/allartk/leaflet.offline/blob/master/docs/api.md#new-tilelayeroffline"
target="_blank"
>layer</a
>
and control provided by leaflet.offline. Click the buttons on the left
side to download tiles from the visible area at zoom levels 13 & 16
(see the script). Enable the offline tiles layer to see what is
already offline available.
</p>
<p>
Total in database:
<span id="storage"></span> files
</p>
<div id="progress-wrapper">
<div class="">Download Progress</div>
<div class="flex-grow-1 ml-2 my-1">
<div id="progress" class="progress">
<div
id="progressbar"
role="progressbar"
style="width: 0%"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
></div>
</div>
</div>
</div>
<div id="map" style="height: 75vh"></div>
</main>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View file

@ -0,0 +1,940 @@
{
"name": "map-layers-and-control",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "map-layers-and-control",
"version": "0.0.0",
"dependencies": {
"@fortawesome/fontawesome-free": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"debounce": "^2.2.0",
"idb": "^8.0.0",
"leaflet": "^1.9.4",
"leaflet.offline": "^3.1.0"
},
"devDependencies": {
"prettier": "^3.3.3",
"typescript": "~5.6.2",
"vite": "^5.4.19"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
"cpu": [
"mips64el"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz",
"integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/fontawesome-free": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.6.0.tgz",
"integrity": "sha512-60G28ke/sXdtS9KZCpZSHHkCbdsOGEhIUGlwq6yhY74UpTiToIh8np7A8yphhM4BWsvNFtIvLpi4co+h9Mr9Ow==",
"license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
"integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==",
"license": "(CC-BY-4.0 AND MIT)",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.6.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.3.tgz",
"integrity": "sha512-ufb2CH2KfBWPJok95frEZZ82LtDl0A6QKTa8MoM+cWwDZvVGl5/jNb79pIhRvAalUu+7LD91VYR0nwRD799HkQ==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.3.tgz",
"integrity": "sha512-iAHpft/eQk9vkWIV5t22V77d90CRofgR2006UiCjHcHJFVI1E0oBkQIAbz+pLtthFw3hWEmVB4ilxGyBf48i2Q==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.3.tgz",
"integrity": "sha512-QPW2YmkWLlvqmOa2OwrfqLJqkHm7kJCIMq9kOz40Zo9Ipi40kf9ONG5Sz76zszrmIZZ4hgRIkez69YnTHgEz1w==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.3.tgz",
"integrity": "sha512-KO0pN5x3+uZm1ZXeIfDqwcvnQ9UEGN8JX5ufhmgH5Lz4ujjZMAnxQygZAVGemFWn+ZZC0FQopruV4lqmGMshow==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.24.3.tgz",
"integrity": "sha512-CsC+ZdIiZCZbBI+aRlWpYJMSWvVssPuWqrDy/zi9YfnatKKSLFCe6fjna1grHuo/nVaHG+kiglpRhyBQYRTK4A==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.24.3.tgz",
"integrity": "sha512-F0nqiLThcfKvRQhZEzMIXOQG4EeX61im61VYL1jo4eBxv4aZRmpin6crnBJQ/nWnCsjH5F6J3W6Stdm0mBNqBg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.3.tgz",
"integrity": "sha512-KRSFHyE/RdxQ1CSeOIBVIAxStFC/hnBgVcaiCkQaVC+EYDtTe4X7z5tBkFyRoBgUGtB6Xg6t9t2kulnX6wJc6A==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.3.tgz",
"integrity": "sha512-h6Q8MT+e05zP5BxEKz0vi0DhthLdrNEnspdLzkoFqGwnmOzakEHSlXfVyA4HJ322QtFy7biUAVFPvIDEDQa6rw==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.3.tgz",
"integrity": "sha512-fKElSyXhXIJ9pqiYRqisfirIo2Z5pTTve5K438URf08fsypXrEkVmShkSfM8GJ1aUyvjakT+fn2W7Czlpd/0FQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.3.tgz",
"integrity": "sha512-YlddZSUk8G0px9/+V9PVilVDC6ydMz7WquxozToozSnfFK6wa6ne1ATUjUvjin09jp34p84milxlY5ikueoenw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.3.tgz",
"integrity": "sha512-yNaWw+GAO8JjVx3s3cMeG5Esz1cKVzz8PkTJSfYzE5u7A+NvGmbVFEHP+BikTIyYWuz0+DX9kaA3pH9Sqxp69g==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.3.tgz",
"integrity": "sha512-lWKNQfsbpv14ZCtM/HkjCTm4oWTKTfxPmr7iPfp3AHSqyoTz5AgLemYkWLwOBWc+XxBbrU9SCokZP0WlBZM9lA==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.3.tgz",
"integrity": "sha512-HoojGXTC2CgCcq0Woc/dn12wQUlkNyfH0I1ABK4Ni9YXyFQa86Fkt2Q0nqgLfbhkyfQ6003i3qQk9pLh/SpAYw==",
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.3.tgz",
"integrity": "sha512-mnEOh4iE4USSccBOtcrjF5nj+5/zm6NcNhbSEfR3Ot0pxBwvEn5QVUXcuOwwPkapDtGZ6pT02xLoPaNv06w7KQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.3.tgz",
"integrity": "sha512-rMTzawBPimBQkG9NKpNHvquIUTQPzrnPxPbCY1Xt+mFkW7pshvyIS5kYgcf74goxXOQk0CP3EoOC1zcEezKXhw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.3.tgz",
"integrity": "sha512-2lg1CE305xNvnH3SyiKwPVsTVLCg4TmNCF1z7PSHX2uZY2VbUpdkgAllVoISD7JO7zu+YynpWNSKAtOrX3AiuA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.3.tgz",
"integrity": "sha512-9SjYp1sPyxJsPWuhOCX6F4jUMXGbVVd5obVpoVEi8ClZqo52ViZewA6eFz85y8ezuOA+uJMP5A5zo6Oz4S5rVQ==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.3.tgz",
"integrity": "sha512-HGZgRFFYrMrP3TJlq58nR1xy8zHKId25vhmm5S9jETEfDf6xybPxsavFTJaufe2zgOGYJBskGlj49CwtEuFhWQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true
},
"node_modules/debounce": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-2.2.0.tgz",
"integrity": "sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"dev": true,
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.21.5",
"@esbuild/android-arm": "0.21.5",
"@esbuild/android-arm64": "0.21.5",
"@esbuild/android-x64": "0.21.5",
"@esbuild/darwin-arm64": "0.21.5",
"@esbuild/darwin-x64": "0.21.5",
"@esbuild/freebsd-arm64": "0.21.5",
"@esbuild/freebsd-x64": "0.21.5",
"@esbuild/linux-arm": "0.21.5",
"@esbuild/linux-arm64": "0.21.5",
"@esbuild/linux-ia32": "0.21.5",
"@esbuild/linux-loong64": "0.21.5",
"@esbuild/linux-mips64el": "0.21.5",
"@esbuild/linux-ppc64": "0.21.5",
"@esbuild/linux-riscv64": "0.21.5",
"@esbuild/linux-s390x": "0.21.5",
"@esbuild/linux-x64": "0.21.5",
"@esbuild/netbsd-x64": "0.21.5",
"@esbuild/openbsd-x64": "0.21.5",
"@esbuild/sunos-x64": "0.21.5",
"@esbuild/win32-arm64": "0.21.5",
"@esbuild/win32-ia32": "0.21.5",
"@esbuild/win32-x64": "0.21.5"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/idb": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/idb/-/idb-8.0.0.tgz",
"integrity": "sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw=="
},
"node_modules/leaflet": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA=="
},
"node_modules/leaflet.offline": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leaflet.offline/-/leaflet.offline-3.1.0.tgz",
"integrity": "sha512-dl3mzTEl1SnmzvJtJ0hVFLvlFX2wut/srvRAZ3A3g7Ee/RQkOo5zQ6tfVNhKe8N+adGXqcdEqEY9kgUxXCnEAw==",
"dependencies": {
"idb": "^7.1.1",
"leaflet": "^1.6.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/leaflet.offline/node_modules/idb": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
"integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ=="
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true
},
"node_modules/postcss": {
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.1.0",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/prettier": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/rollup": {
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.3.tgz",
"integrity": "sha512-HBW896xR5HGmoksbi3JBDtmVzWiPAYqp7wip50hjQ67JbDz61nyoMPdqu1DvVW9asYb2M65Z20ZHsyJCMqMyDg==",
"dev": true,
"dependencies": {
"@types/estree": "1.0.6"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.24.3",
"@rollup/rollup-android-arm64": "4.24.3",
"@rollup/rollup-darwin-arm64": "4.24.3",
"@rollup/rollup-darwin-x64": "4.24.3",
"@rollup/rollup-freebsd-arm64": "4.24.3",
"@rollup/rollup-freebsd-x64": "4.24.3",
"@rollup/rollup-linux-arm-gnueabihf": "4.24.3",
"@rollup/rollup-linux-arm-musleabihf": "4.24.3",
"@rollup/rollup-linux-arm64-gnu": "4.24.3",
"@rollup/rollup-linux-arm64-musl": "4.24.3",
"@rollup/rollup-linux-powerpc64le-gnu": "4.24.3",
"@rollup/rollup-linux-riscv64-gnu": "4.24.3",
"@rollup/rollup-linux-s390x-gnu": "4.24.3",
"@rollup/rollup-linux-x64-gnu": "4.24.3",
"@rollup/rollup-linux-x64-musl": "4.24.3",
"@rollup/rollup-win32-arm64-msvc": "4.24.3",
"@rollup/rollup-win32-ia32-msvc": "4.24.3",
"@rollup/rollup-win32-x64-msvc": "4.24.3",
"fsevents": "~2.3.2"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/typescript": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/vite": {
"version": "5.4.19",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz",
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
"rollup": "^4.20.0"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || >=20.0.0",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
}
}
}
}
}

View file

@ -0,0 +1,25 @@
{
"name": "map-layers-and-control",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"format": "prettier -w ."
},
"devDependencies": {
"prettier": "^3.3.3",
"typescript": "~5.6.2",
"vite": "^5.4.19"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"debounce": "^2.2.0",
"idb": "^8.0.0",
"leaflet": "^1.9.4",
"leaflet.offline": "^3.1.0"
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,84 @@
import { tileLayerOffline, savetiles, SaveStatus } from 'leaflet.offline';
import { Control, Map } from 'leaflet';
import debounce from 'debounce';
import storageLayer from './storageLayer';
import './style.css';
const urlTemplate = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
const leafletMap = new Map('map');
// offline baselayer, will use offline source if available
const baseLayer = tileLayerOffline(urlTemplate, {
attribution: 'Map data {attribution.OpenStreetMap}',
subdomains: 'abc',
minZoom: 13,
}).addTo(leafletMap);
// add buttons to save tiles in area viewed
const saveControl = savetiles(baseLayer, {
zoomlevels: [13, 16], // optional zoomlevels to save, default current zoomlevel
alwaysDownload: false,
confirm(status: SaveStatus, successCallback: Function) {
// eslint-disable-next-line no-alert
if (window.confirm(`Save ${status._tilesforSave.length}`)) {
successCallback();
}
},
confirmRemoval(status: SaveStatus, successCallback: Function) {
// eslint-disable-next-line no-alert
if (window.confirm('Remove all the tiles?')) {
successCallback();
}
},
saveText: '<i class="fa fa-download" title="Save tiles"></i>',
rmText: '<i class="fa fa-trash" title="Remove tiles"></i>',
});
saveControl.addTo(leafletMap);
leafletMap.setView(
{
lat: 52.09,
lng: 5.118,
},
16,
);
// layer switcher control
const layerswitcher = new Control.Layers(
{
'osm (offline)': baseLayer,
},
undefined,
{ collapsed: false },
).addTo(leafletMap);
// add storage overlay
storageLayer(baseLayer, layerswitcher);
// events while saving a tile layer
let progress: number;
let total: number;
const showProgress = debounce(() => {
document.getElementById('progressbar')!.style.width = `${
(progress / total) * 100
}%`;
document.getElementById('progressbar')!.innerHTML = progress.toString();
if (progress === total) {
setTimeout(
() =>
document.getElementById('progress-wrapper')!.classList.remove('show'),
1000,
);
}
}, 10);
baseLayer.on('savestart', (e) => {
progress = 0;
// @ts-ignore
total = e._tilesforSave.length;
document.getElementById('progress-wrapper')!.classList.add('show');
document.getElementById('progressbar')!.style.width = '0%';
});
baseLayer.on('loadtileend', () => {
progress += 1;
showProgress();
});

View file

@ -0,0 +1,34 @@
import { geoJSON } from 'leaflet';
import { getStorageInfo, getStoredTilesAsJson } from 'leaflet.offline';
const urlTemplate = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
export default function storageLayer(baseLayer, layerswitcher) {
let layer;
const getGeoJsonData = () =>
getStorageInfo(urlTemplate).then((tiles) =>
getStoredTilesAsJson(baseLayer.getTileSize(), tiles),
);
const addStorageLayer = () => {
getGeoJsonData().then((geojson) => {
layer = geoJSON(geojson).bindPopup(
(clickedLayer) => clickedLayer.feature.properties.key,
);
layerswitcher.addOverlay(layer, 'offline tiles');
});
};
addStorageLayer();
baseLayer.on('storagesize', (e) => {
document.getElementById('storage').innerHTML = e.storagesize;
if (layer) {
layer.clearLayers();
getGeoJsonData().then((data) => {
layer.addData(data);
});
}
});
}

View file

@ -0,0 +1,26 @@
@import 'leaflet/dist/leaflet.css';
@import '/node_modules/@fortawesome/fontawesome-free/css/all.min.css';
body {
margin: 0;
font-family: sans-serif;
}
#app {
height: 100vh;
max-width: 1200px;
margin: 0 auto;
}
#progress-wrapper {
padding: 1em 0;
display: none;
&.show {
display: block;
}
}
#progressbar {
background-color: gray;
height: 1em;
}

View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

View file

@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View file

@ -0,0 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Custom Layer (WMTS)</title>
</head>
<body>
<div class="container-fluid py-md-3">
<main id="app">
<h1>WMTS</h1>
<p>
This leaflet WMTS layer uses the download & save methods from the
<a
href="https://github.com/allartk/leaflet.offline/blob/master/docs/api.md#tilemanagerdownloadtiletileurl--promiseblob"
target="_blank"
>API</a
>
to save all viewed tiles for offline usage.
</p>
<div id="map" style="height: 75vh"></div>
</main>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View file

@ -0,0 +1,952 @@
{
"name": "map-wmts",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "map-wmts",
"version": "0.0.0",
"devDependencies": {
"idb": "^8.0.0",
"leaflet": "^1.9.4",
"leaflet.offline": "^3.1.0",
"prettier": "^3.3.3",
"typescript": "~5.6.2",
"vite": "^5.4.19"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.27.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.27.2.tgz",
"integrity": "sha512-Tj+j7Pyzd15wAdSJswvs5CJzJNV+qqSUcr/aCD+jpQSBtXvGnV0pnrjoc8zFTe9fcKCatkpFpOO7yAzpO998HA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.27.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.27.2.tgz",
"integrity": "sha512-xsPeJgh2ThBpUqlLgRfiVYBEf/P1nWlWvReG+aBWfNv3XEBpa6ZCmxSVnxJgLgkNz4IbxpLy64h2gCmAAQLneQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.27.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.27.2.tgz",
"integrity": "sha512-KnXU4m9MywuZFedL35Z3PuwiTSn/yqRIhrEA9j+7OSkji39NzVkgxuxTYg5F8ryGysq4iFADaU5osSizMXhU2A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.27.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.27.2.tgz",
"integrity": "sha512-Hj77A3yTvUeCIx/Vi+4d4IbYhyTwtHj07lVzUgpUq9YpJSEiGJj4vXMKwzJ3w5zp5v3PFvpJNgc/J31smZey6g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.27.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.27.2.tgz",
"integrity": "sha512-RjgKf5C3xbn8gxvCm5VgKZ4nn0pRAIe90J0/fdHUsgztd3+Zesb2lm2+r6uX4prV2eUByuxJNdt647/1KPRq5g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.27.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.27.2.tgz",
"integrity": "sha512-duq21FoXwQtuws+V9H6UZ+eCBc7fxSpMK1GQINKn3fAyd9DFYKPJNcUhdIKOrMFjLEJgQskoMoiuizMt+dl20g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.27.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.27.2.tgz",
"integrity": "sha512-6npqOKEPRZkLrMcvyC/32OzJ2srdPzCylJjiTJT2c0bwwSGm7nz2F9mNQ1WrAqCBZROcQn91Fno+khFhVijmFA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.27.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.27.2.tgz",
"integrity": "sha512-V9Xg6eXtgBtHq2jnuQwM/jr2mwe2EycnopO8cbOvpzFuySCGtKlPCI3Hj9xup/pJK5Q0388qfZZy2DqV2J8ftw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.27.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.27.2.tgz",
"integrity": "sha512-uCFX9gtZJoQl2xDTpRdseYuNqyKkuMDtH6zSrBTA28yTfKyjN9hQ2B04N5ynR8ILCoSDOrG/Eg+J2TtJ1e/CSA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.27.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.27.2.tgz",
"integrity": "sha512-/PU9P+7Rkz8JFYDHIi+xzHabOu9qEWR07L5nWLIUsvserrxegZExKCi2jhMZRd0ATdboKylu/K5yAXbp7fYFvA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.27.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.27.2.tgz",
"integrity": "sha512-eCHmol/dT5odMYi/N0R0HC8V8QE40rEpkyje/ZAXJYNNoSfrObOvG/Mn+s1F/FJyB7co7UQZZf6FuWnN6a7f4g==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.27.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.27.2.tgz",
"integrity": "sha512-DEP3Njr9/ADDln3kNi76PXonLMSSMiCir0VHXxmGSHxCxDfQ70oWjHcJGfiBugzaqmYdTC7Y+8Int6qbnxPBIQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.27.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.27.2.tgz",
"integrity": "sha512-NHGo5i6IE/PtEPh5m0yw5OmPMpesFnzMIS/lzvN5vknnC1sXM5Z/id5VgcNPgpD+wHmIcuYYgW+Q53v+9s96lQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.27.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.27.2.tgz",
"integrity": "sha512-PaW2DY5Tan+IFvNJGHDmUrORadbe/Ceh8tQxi8cmdQVCCYsLoQo2cuaSj+AU+YRX8M4ivS2vJ9UGaxfuNN7gmg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.27.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.27.2.tgz",
"integrity": "sha512-dOlWEMg2gI91Qx5I/HYqOD6iqlJspxLcS4Zlg3vjk1srE67z5T2Uz91yg/qA8sY0XcwQrFzWWiZhMNERylLrpQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.27.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.27.2.tgz",
"integrity": "sha512-euMIv/4x5Y2/ImlbGl88mwKNXDsvzbWUlT7DFky76z2keajCtcbAsN9LUdmk31hAoVmJJYSThgdA0EsPeTr1+w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.27.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.27.2.tgz",
"integrity": "sha512-RsnE6LQkUHlkC10RKngtHNLxb7scFykEbEwOFDjr3CeCMG+Rr+cKqlkKc2/wJ1u4u990urRHCbjz31x84PBrSQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.27.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.27.2.tgz",
"integrity": "sha512-foJM5vv+z2KQmn7emYdDLyTbkoO5bkHZE1oth2tWbQNGW7mX32d46Hz6T0MqXdWS2vBZhaEtHqdy9WYwGfiliA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true,
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.21.5",
"@esbuild/android-arm": "0.21.5",
"@esbuild/android-arm64": "0.21.5",
"@esbuild/android-x64": "0.21.5",
"@esbuild/darwin-arm64": "0.21.5",
"@esbuild/darwin-x64": "0.21.5",
"@esbuild/freebsd-arm64": "0.21.5",
"@esbuild/freebsd-x64": "0.21.5",
"@esbuild/linux-arm": "0.21.5",
"@esbuild/linux-arm64": "0.21.5",
"@esbuild/linux-ia32": "0.21.5",
"@esbuild/linux-loong64": "0.21.5",
"@esbuild/linux-mips64el": "0.21.5",
"@esbuild/linux-ppc64": "0.21.5",
"@esbuild/linux-riscv64": "0.21.5",
"@esbuild/linux-s390x": "0.21.5",
"@esbuild/linux-x64": "0.21.5",
"@esbuild/netbsd-x64": "0.21.5",
"@esbuild/openbsd-x64": "0.21.5",
"@esbuild/sunos-x64": "0.21.5",
"@esbuild/win32-arm64": "0.21.5",
"@esbuild/win32-ia32": "0.21.5",
"@esbuild/win32-x64": "0.21.5"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/idb": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/idb/-/idb-8.0.0.tgz",
"integrity": "sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw==",
"dev": true,
"license": "ISC"
},
"node_modules/leaflet": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/leaflet.offline": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leaflet.offline/-/leaflet.offline-3.1.0.tgz",
"integrity": "sha512-dl3mzTEl1SnmzvJtJ0hVFLvlFX2wut/srvRAZ3A3g7Ee/RQkOo5zQ6tfVNhKe8N+adGXqcdEqEY9kgUxXCnEAw==",
"dev": true,
"license": "ISC",
"dependencies": {
"idb": "^7.1.1",
"leaflet": "^1.6.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/leaflet.offline/node_modules/idb": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
"integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==",
"dev": true,
"license": "ISC"
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.4.49",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/prettier": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/rollup": {
"version": "4.27.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.27.2.tgz",
"integrity": "sha512-KreA+PzWmk2yaFmZVwe6GB2uBD86nXl86OsDkt1bJS9p3vqWuEQ6HnJJ+j/mZi/q0920P99/MVRlB4L3crpF5w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.6"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.27.2",
"@rollup/rollup-android-arm64": "4.27.2",
"@rollup/rollup-darwin-arm64": "4.27.2",
"@rollup/rollup-darwin-x64": "4.27.2",
"@rollup/rollup-freebsd-arm64": "4.27.2",
"@rollup/rollup-freebsd-x64": "4.27.2",
"@rollup/rollup-linux-arm-gnueabihf": "4.27.2",
"@rollup/rollup-linux-arm-musleabihf": "4.27.2",
"@rollup/rollup-linux-arm64-gnu": "4.27.2",
"@rollup/rollup-linux-arm64-musl": "4.27.2",
"@rollup/rollup-linux-powerpc64le-gnu": "4.27.2",
"@rollup/rollup-linux-riscv64-gnu": "4.27.2",
"@rollup/rollup-linux-s390x-gnu": "4.27.2",
"@rollup/rollup-linux-x64-gnu": "4.27.2",
"@rollup/rollup-linux-x64-musl": "4.27.2",
"@rollup/rollup-win32-arm64-msvc": "4.27.2",
"@rollup/rollup-win32-ia32-msvc": "4.27.2",
"@rollup/rollup-win32-x64-msvc": "4.27.2",
"fsevents": "~2.3.2"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/typescript": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/vite": {
"version": "5.4.19",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz",
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
"rollup": "^4.20.0"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || >=20.0.0",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
}
}
}
}
}

View file

@ -0,0 +1,19 @@
{
"name": "map-wmts",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"idb": "^8.0.0",
"leaflet": "^1.9.4",
"leaflet.offline": "^3.1.0",
"prettier": "^3.3.3",
"typescript": "~5.6.2",
"vite": "^5.4.19"
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,51 @@
import { Map, tileLayer } from 'leaflet';
import { getBlobByKey, downloadTile, saveTile } from 'leaflet.offline';
import './style.css'
export const wmtsUrlTemplate =
'https://service.pdok.nl/brt/achtergrondkaart/wmts/v2_0?service=WMTS&request=GetTile&version=1.0.0&tilematrixset=EPSG:3857&layer=standaard&tilematrix={z}&tilerow={y}&tilecol={x}&format=image%2Fpng';
const leafletMap = new Map('map');
const brtLayer = tileLayer(wmtsUrlTemplate);
brtLayer.addTo(leafletMap);
brtLayer.on('tileloadstart', (event) => {
const { tile } = event;
const url = tile.src;
// reset tile.src, to not start download yet
tile.src = '';
getBlobByKey(url).then((blob) => {
if (blob) {
tile.src = URL.createObjectURL(blob);
console.debug(`Loaded ${url} from idb`);
return;
}
tile.src = url;
// create helper function for it?
const { x, y, z } = event.coords;
const { _url: urlTemplate } = event.target;
const tileInfo = {
key: url,
url,
x,
y,
z,
urlTemplate,
createdAt: Date.now(),
};
downloadTile(url)
.then((dl) => saveTile(tileInfo, dl))
.then(() => console.debug(`Saved ${url} in idb`));
});
});
leafletMap.setView(
{
lat: 52.09,
lng: 5.118,
},
16
);

View file

@ -0,0 +1,12 @@
@import "leaflet/dist/leaflet.css";
body {
margin: 0;
font-family: sans-serif;
}
#app {
height: 100vh;
max-width: 1200px;
margin: 0 auto;
}

View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

View file

@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

92
pwa/node_modules/leaflet.offline/karma.conf.ts generated vendored Normal file
View file

@ -0,0 +1,92 @@
const typescript = require('@rollup/plugin-typescript');
const { nodeResolve } = require('@rollup/plugin-node-resolve');
const commonjs = require('@rollup/plugin-commonjs');
const istanbul = require('rollup-plugin-istanbul');
const extensions = ['.js', '.ts'];
process.env.CHROME_BIN = require('puppeteer').executablePath()
module.exports = (config) => {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['mocha'],
// list of files / patterns to load in the browser
files: ['test/*.ts'],
// list of files to exclude
exclude: [],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'test/*.ts': ['rollup'],
},
rollupPreprocessor: {
output: {
format: 'iife',
name: 'LeafletOffline',
sourcemap: 'inline',
},
plugins: [
commonjs(),
nodeResolve({ extensions }),
typescript({ declaration: false, declarationDir: undefined }),
istanbul({ exclude: ['test/*.ts', 'node_modules/**/*'] }),
],
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['mocha', 'progress', 'coverage'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['ChromeHeadless','ChromeHeadlessWithoutSandbox'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true,
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity,
// damn ubuntu snap https://github.com/karma-runner/karma-firefox-launcher/issues/183#issuecomment-1283875784
customLaunchers: {
ChromeHeadlessWithoutSandbox: {
base: 'Chrome',
flags: ['--no-sandbox','--headless'],
displayName: 'Chrome w/o sandbox'
}
},
coverageReporter: {
reporters: [
{
type: 'html',
dir: 'coverage/',
},
{ type: 'lcov' },
],
},
});
};

Binary file not shown.

93
pwa/node_modules/leaflet.offline/package.json generated vendored Normal file
View file

@ -0,0 +1,93 @@
{
"name": "leaflet.offline",
"version": "3.2.1",
"description": "Offline tilelayer for leaflet",
"main": "dist/bundle.js",
"engines": {
"node": ">=16"
},
"types": "dist/types/src/index.d.ts",
"scripts": {
"type-check": "tsc --noEmit",
"docs:build": "typedoc --out docs src/index.ts",
"docs:deploy": "gh-pages -d docs",
"build": "rm -rf dist && rollup -c",
"lint": "eslint && prettier --check \"./(src|test)/**/*.ts\"",
"lint:fix": "eslint --fix src test",
"format": "eslint --fix src test && prettier -w \"./(src|test)/**/*.ts\"",
"test": "karma start --browsers ChromeHeadless",
"testci": "karma start --browsers ChromeHeadlessWithoutSandbox",
"test:watch": "karma start --no-single-run",
"watch": "rollup -c -w",
"preversion": "karma start --browsers ChromeHeadlessWithoutSandbox --single-run",
"prepare": "husky || true",
"prepublishOnly": "npm run build && npx husky install"
},
"repository": {
"type": "git",
"url": "git+https://github.com/allartk/leaflet.offline.git"
},
"keywords": [
"leaflet",
"offline"
],
"author": "Allart Kooiman",
"license": "ISC",
"bugs": {
"url": "https://github.com/allartk/leaflet.offline/issues"
},
"homepage": "https://github.com/allartk/leaflet.offline",
"devDependencies": {
"@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^19.2.2",
"@rollup/plugin-commonjs": "^28.0.6",
"@rollup/plugin-node-resolve": "^15.0.2",
"@rollup/plugin-typescript": "^12.1.4",
"@types/geojson": "^7946.0.8",
"@types/karma-chai": "^0.1.3",
"@types/karma-chai-sinon": "^0.1.16",
"@types/leaflet": "^1.7.9",
"@types/mocha": "^10.0.7",
"@types/sinon": "^17.0.3",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"chai": "^6.2.2",
"eslint": "^8.57.1",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^10.0.1",
"fetch-mock": "^12.5.3",
"gh-pages": "^6.3.0",
"husky": "^9.0.11",
"karma": "^6.4.2",
"karma-chrome-launcher": "^3.2.0",
"karma-coverage": "^2.2.1",
"karma-firefox-launcher": "^2.1.2",
"karma-mocha": "^2.0.1",
"karma-mocha-reporter": "^2.2.5",
"karma-rollup-preprocessor": "^7.0.5",
"leaflet": "^1.7.1",
"leaflet.vectorgrid": "^1.1.0",
"lint-staged": "^16.2.3",
"mocha": "^11.7.5",
"npm-run-all": "^4.1.5",
"prettier": "^3.0.3",
"puppeteer": "^24.10.2",
"rollup": "^4.53.3",
"rollup-plugin-istanbul": "^5.0.0",
"sinon": "^21.0.0",
"typedoc": "^0.26.4",
"typedoc-plugin-markdown": "^4.2.10",
"typescript": "^5.0.4"
},
"lint-staged": {
"src/**/*.js": [
"eslint --cache --fix",
"prettier --write"
]
},
"dependencies": {
"idb": "^8.0.2",
"leaflet": "^1.6.0"
}
}

20
pwa/node_modules/leaflet.offline/rollup.config.mjs generated vendored Normal file
View file

@ -0,0 +1,20 @@
import resolve from '@rollup/plugin-node-resolve';
import pkg from './package.json' with { type: 'json' };
import typescript from '@rollup/plugin-typescript';
const extensions = ['.ts'];
export default {
input: 'src/index.ts',
output: {
file: pkg.main,
format: 'umd',
name: 'LeafletOffline',
globals: {
leaflet: 'L',
idb: 'idb',
},
},
plugins: [resolve({ extensions }), typescript()],
external: ['leaflet', 'idb'],
};

View file

@ -0,0 +1,267 @@
import {
Control,
ControlOptions,
DomEvent,
DomUtil,
LatLngBounds,
bounds,
Map,
} from 'leaflet';
import { TileLayerOffline } from './TileLayerOffline';
import {
truncate,
getStorageLength,
downloadTile,
saveTile,
TileInfo,
hasTile,
} from './TileManager';
export interface SaveTileOptions extends ControlOptions {
saveText: string;
rmText: string;
maxZoom: number;
saveWhatYouSee: boolean;
bounds: LatLngBounds | null;
confirm: ((status: SaveStatus, successCallback: Function) => void) | null;
confirmRemoval:
| ((status: SaveStatus, successCallback: Function) => void)
| null;
parallel: number;
zoomlevels?: number[];
alwaysDownload: boolean;
}
export interface SaveStatus {
_tilesforSave: TileInfo[];
storagesize: number;
lengthToBeSaved: number;
lengthSaved: number;
lengthLoaded: number;
}
export class ControlSaveTiles extends Control {
_map!: Map;
_refocusOnMap!: DomEvent.EventHandlerFn;
_baseLayer!: TileLayerOffline;
options: SaveTileOptions;
status: SaveStatus = {
storagesize: 0,
lengthToBeSaved: 0,
lengthSaved: 0,
lengthLoaded: 0,
_tilesforSave: [],
};
constructor(baseLayer: TileLayerOffline, options: Partial<SaveTileOptions>) {
super(options);
this._baseLayer = baseLayer;
this.setStorageSize();
this.options = {
...{
position: 'topleft',
saveText: '+',
rmText: '-',
maxZoom: 19,
saveWhatYouSee: false,
bounds: null,
confirm: null,
confirmRemoval: null,
parallel: 50,
zoomlevels: undefined,
alwaysDownload: true,
},
...options,
};
}
setStorageSize() {
if (this.status.storagesize) {
return Promise.resolve(this.status.storagesize);
}
return getStorageLength()
.then((numberOfKeys) => {
this.status.storagesize = numberOfKeys;
this._baseLayer.fire('storagesize', this.status);
return numberOfKeys;
})
.catch(() => 0);
}
getStorageSize(callback: Function) {
this.setStorageSize().then((result) => {
if (callback) {
callback(result);
}
});
}
setLayer(layer: TileLayerOffline) {
this._baseLayer = layer;
}
onAdd() {
const container = DomUtil.create('div', 'savetiles leaflet-bar');
const { options } = this;
this._createButton(
options.saveText,
'savetiles',
container,
this._saveTiles,
);
this._createButton(options.rmText, 'rmtiles', container, this._rmTiles);
return container;
}
_createButton(
html: string,
className: string,
container: HTMLElement,
fn: DomEvent.EventHandlerFn,
) {
const link = DomUtil.create('a', className, container);
link.innerHTML = html;
link.href = '#';
link.ariaRoleDescription = 'button';
DomEvent.on(link, 'mousedown dblclick', DomEvent.stopPropagation)
.on(link, 'click', DomEvent.stop)
.on(link, 'click', fn, this)
.on(link, 'click', this._refocusOnMap, this);
return link;
}
_saveTiles() {
const tiles = this._calculateTiles();
this._resetStatus(tiles);
const successCallback = async () => {
this._baseLayer.fire('savestart', this.status);
const loader = async (): Promise<void> => {
const tile = tiles.shift();
if (tile === undefined) {
return Promise.resolve();
}
const blob = await this._loadTile(tile);
if (blob) {
await this._saveTile(tile, blob);
}
return loader();
};
const parallel = Math.min(tiles.length, this.options.parallel);
for (let i = 0; i < parallel; i += 1) {
loader();
}
};
if (this.options.confirm) {
this.options.confirm(this.status, successCallback);
} else {
successCallback();
}
}
_calculateTiles() {
let tiles: TileInfo[] = [];
// minimum zoom to prevent the user from saving the whole world
const minZoom = 5;
// current zoom or zoom options
let zoomlevels = [];
if (this.options.saveWhatYouSee) {
const currentZoom = this._map.getZoom();
if (currentZoom < minZoom) {
throw new Error(
`It's not possible to save with zoom below level ${minZoom}.`,
);
}
const { maxZoom } = this.options;
for (let zoom = currentZoom; zoom <= maxZoom; zoom += 1) {
zoomlevels.push(zoom);
}
} else {
zoomlevels = this.options.zoomlevels || [this._map.getZoom()];
}
const latlngBounds = this.options.bounds || this._map.getBounds();
for (let i = 0; i < zoomlevels.length; i += 1) {
const area = bounds(
this._map.project(latlngBounds.getNorthWest(), zoomlevels[i]),
this._map.project(latlngBounds.getSouthEast(), zoomlevels[i]),
);
tiles = tiles.concat(this._baseLayer.getTileUrls(area, zoomlevels[i]));
}
return tiles;
}
_resetStatus(tiles: TileInfo[]) {
this.status = {
lengthLoaded: 0,
lengthToBeSaved: tiles.length,
lengthSaved: 0,
_tilesforSave: tiles,
storagesize: this.status.storagesize,
};
}
async _loadTile(tile: TileInfo) {
let blob;
if (
this.options.alwaysDownload === true ||
(await hasTile(tile.key)) === false
) {
blob = await downloadTile(tile.url);
}
this.status.lengthLoaded += 1;
this._baseLayer.fire('loadtileend', this.status);
if (this.status.lengthLoaded === this.status.lengthToBeSaved) {
this._baseLayer.fire('loadend', this.status);
}
return blob;
}
async _saveTile(tile: TileInfo, blob: Blob): Promise<void> {
await saveTile(tile, blob);
this.status.lengthSaved += 1;
this._baseLayer.fire('savetileend', this.status);
if (this.status.lengthSaved === this.status.lengthToBeSaved) {
this._baseLayer.fire('saveend', this.status);
this.setStorageSize();
}
}
_rmTiles() {
const successCallback = () => {
truncate().then(() => {
this.status.storagesize = 0;
this._baseLayer.fire('tilesremoved');
this._baseLayer.fire('storagesize', this.status);
});
};
if (this.options.confirmRemoval) {
this.options.confirmRemoval(this.status, successCallback);
} else {
successCallback();
}
}
}
export function savetiles(
baseLayer: TileLayerOffline,
options: Partial<SaveTileOptions>,
) {
return new ControlSaveTiles(baseLayer, options);
}
/** @ts-ignore */
if (window.L) {
/** @ts-ignore */
window.L.control.savetiles = savetiles;
}

View file

@ -0,0 +1,101 @@
import {
Bounds,
Coords,
DomEvent,
DoneCallback,
TileLayer,
TileLayerOptions,
Util,
} from 'leaflet';
import {
getTileUrl,
TileInfo,
getTilePoints,
getTileImageSource,
} from './TileManager';
export class TileLayerOffline extends TileLayer {
_url!: string;
createTile(coords: Coords, done: DoneCallback): HTMLElement {
const tile = document.createElement('img');
DomEvent.on(tile, 'load', Util.bind(this._tileOnLoad, this, done, tile));
DomEvent.on(tile, 'error', Util.bind(this._tileOnError, this, done, tile));
if (this.options.crossOrigin || this.options.crossOrigin === '') {
tile.crossOrigin =
this.options.crossOrigin === true ? '' : this.options.crossOrigin;
}
tile.alt = '';
tile.setAttribute('role', 'presentation');
getTileImageSource(
this._getStorageKey(coords),
this.getTileUrl(coords),
).then((src) => (tile.src = src));
return tile;
}
/**
* get key to use for storage
* @private
* @param {string} url url used to load tile
* @return {string} unique identifier.
*/
_getStorageKey(coords: { x: number; y: number; z: number }) {
return getTileUrl(this._url, {
...coords,
...this.options,
// @ts-ignore: Possibly undefined
s: this.options.subdomains['0'],
});
}
/**
* Get tileinfo for zoomlevel & bounds
*/
getTileUrls(bounds: Bounds, zoom: number): TileInfo[] {
const tiles: TileInfo[] = [];
const tilePoints = getTilePoints(bounds, this.getTileSize());
for (let index = 0; index < tilePoints.length; index += 1) {
const tilePoint = tilePoints[index];
const data = {
...this.options,
x: tilePoint.x,
y: tilePoint.y,
z: zoom + (this.options.zoomOffset || 0),
};
tiles.push({
key: getTileUrl(this._url, {
...data,
s: this.options.subdomains?.[0],
}),
url: getTileUrl(this._url, {
...data,
// @ts-ignore: Undefined
s: this._getSubdomain(tilePoint),
}),
z: zoom,
x: tilePoint.x,
y: tilePoint.y,
urlTemplate: this._url,
createdAt: Date.now(),
});
}
return tiles;
}
}
export function tileLayerOffline(url: string, options: TileLayerOptions) {
return new TileLayerOffline(url, options);
}
/** @ts-ignore */
if (window.L) {
/** @ts-ignore */
window.L.tileLayer.offline = tileLayerOffline;
}

226
pwa/node_modules/leaflet.offline/src/TileManager.ts generated vendored Normal file
View file

@ -0,0 +1,226 @@
/**
* Api methods used in control and layer
* For advanced usage
*
* @module TileManager
*
*/
import { Bounds, Browser, CRS, Point, Util } from 'leaflet';
import { openDB, deleteDB, IDBPDatabase } from 'idb';
import { FeatureCollection, Polygon } from 'geojson';
export type TileInfo = {
key: string;
url: string;
urlTemplate: string;
x: number;
y: number;
z: number;
createdAt: number;
};
export type StoredTile = TileInfo & { blob: Blob };
const tileStoreName = 'tileStore';
const urlTemplateIndex = 'urlTemplate';
let dbPromise: Promise<IDBPDatabase> | undefined;
export function openTilesDataBase(): Promise<IDBPDatabase> {
if (dbPromise) {
return dbPromise;
}
dbPromise = openDB('leaflet.offline', 2, {
upgrade(db, oldVersion) {
deleteDB('leaflet_offline');
deleteDB('leaflet_offline_areas');
if (oldVersion < 1) {
const tileStore = db.createObjectStore(tileStoreName, {
keyPath: 'key',
});
tileStore.createIndex(urlTemplateIndex, 'urlTemplate');
tileStore.createIndex('z', 'z');
}
},
});
return dbPromise;
}
/**
* @example
* ```js
* import { getStorageLength } from 'leaflet.offline'
* getStorageLength().then(i => console.log(i + 'tiles in storage'))
* ```
*/
export async function getStorageLength(): Promise<number> {
const db = await openTilesDataBase();
return db.count(tileStoreName);
}
/**
* @example
* ```js
* import { getStorageInfo } from 'leaflet.offline'
* getStorageInfo('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png')
* ```
*/
export async function getStorageInfo(
urlTemplate: string,
): Promise<StoredTile[]> {
const range = IDBKeyRange.only(urlTemplate);
const db = await openTilesDataBase();
return db.getAllFromIndex(tileStoreName, urlTemplateIndex, range);
}
/**
* @example
* ```js
* import { downloadTile } from 'leaflet.offline'
* downloadTile(tileInfo.url).then(blob => saveTile(tileInfo, blob))
* ```
*/
export async function downloadTile(tileUrl: string): Promise<Blob> {
const response = await fetch(tileUrl);
if (!response.ok) {
throw new Error(`Request failed with status ${response.statusText}`);
}
return response.blob();
}
/**
* @example
* ```js
* saveTile(tileInfo, blob).then(() => console.log(`saved tile from ${tileInfo.url}`))
* ```
*/
export async function saveTile(
tileInfo: TileInfo,
blob: Blob,
): Promise<IDBValidKey> {
const db = await openTilesDataBase();
return db.put(tileStoreName, {
blob,
...tileInfo,
});
}
export function getTileUrl(urlTemplate: string, data: any): string {
return Util.template(urlTemplate, {
...data,
r: Browser.retina ? '@2x' : '',
});
}
export function getTilePoints(area: Bounds, tileSize: Point): Point[] {
const points: Point[] = [];
if (!area.min || !area.max) {
return points;
}
const topLeftTile = area.min.divideBy(tileSize.x).floor();
const bottomRightTile = area.max.divideBy(tileSize.x).floor();
for (let j = topLeftTile.y; j <= bottomRightTile.y; j += 1) {
for (let i = topLeftTile.x; i <= bottomRightTile.x; i += 1) {
points.push(new Point(i, j));
}
}
return points;
}
/**
* Get a geojson of tiles from one resource
*
* @example
* const urlTemplate = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
* const getGeoJsonData = () => LeafletOffline.getStorageInfo(urlTemplate)
* .then((data) => LeafletOffline.getStoredTilesAsJson(baseLayer, data));
*
* getGeoJsonData().then((geojson) => {
* storageLayer = L.geoJSON(geojson).bindPopup(
* (clickedLayer) => clickedLayer.feature.properties.key,
* );
* });
*
*/
export function getStoredTilesAsJson(
tileSize: { x: number; y: number },
tiles: TileInfo[],
): FeatureCollection<Polygon> {
const featureCollection: FeatureCollection<Polygon> = {
type: 'FeatureCollection',
features: [],
};
for (let i = 0; i < tiles.length; i += 1) {
const topLeftPoint = new Point(
tiles[i].x * tileSize.x,
tiles[i].y * tileSize.y,
);
const bottomRightPoint = new Point(
topLeftPoint.x + tileSize.x,
topLeftPoint.y + tileSize.y,
);
const topLeftlatlng = CRS.EPSG3857.pointToLatLng(topLeftPoint, tiles[i].z);
const botRightlatlng = CRS.EPSG3857.pointToLatLng(
bottomRightPoint,
tiles[i].z,
);
featureCollection.features.push({
type: 'Feature',
properties: tiles[i],
geometry: {
type: 'Polygon',
coordinates: [
[
[topLeftlatlng.lng, topLeftlatlng.lat],
[botRightlatlng.lng, topLeftlatlng.lat],
[botRightlatlng.lng, botRightlatlng.lat],
[topLeftlatlng.lng, botRightlatlng.lat],
[topLeftlatlng.lng, topLeftlatlng.lat],
],
],
},
});
}
return featureCollection;
}
/**
* Remove tile by key
*/
export async function removeTile(key: string): Promise<void> {
const db = await openTilesDataBase();
return db.delete(tileStoreName, key);
}
/**
* Get single tile blob
*/
export async function getBlobByKey(key: string): Promise<Blob> {
return (await openTilesDataBase())
.get(tileStoreName, key)
.then((result) => result && result.blob);
}
export async function hasTile(key: string): Promise<boolean> {
const db = await openTilesDataBase();
const result = await db.getKey(tileStoreName, key);
return result !== undefined;
}
/**
* Remove everything
*/
export async function truncate(): Promise<void> {
return (await openTilesDataBase()).clear(tileStoreName);
}
export async function getTileImageSource(key: string, url: string) {
const shouldUseUrl = !(await hasTile(key));
if (shouldUseUrl) {
return url;
}
const blob = await getBlobByKey(key);
return URL.createObjectURL(blob);
}

BIN
pwa/node_modules/leaflet.offline/src/images/save.png generated vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 B

23
pwa/node_modules/leaflet.offline/src/index.ts generated vendored Normal file
View file

@ -0,0 +1,23 @@
export { tileLayerOffline } from './TileLayerOffline';
export { savetiles } from './ControlSaveTiles';
export type {
SaveStatus,
ControlSaveTiles,
SaveTileOptions,
} from './ControlSaveTiles';
export type { TileInfo, StoredTile } from './TileManager';
export type { TileLayerOffline } from './TileLayerOffline';
export {
getStorageInfo,
getStorageLength,
getStoredTilesAsJson,
removeTile,
truncate,
downloadTile,
saveTile,
hasTile,
getBlobByKey,
getTilePoints,
getTileUrl,
getTileImageSource,
} from './TileManager';

View file

@ -0,0 +1,143 @@
import { Map } from 'leaflet';
import { assert } from 'chai';
import { ControlSaveTiles, savetiles } from '../src/ControlSaveTiles';
import { TileLayerOffline } from '../src/TileLayerOffline';
import * as sinon from 'sinon';
describe('control with defaults', () => {
let saveControl: ControlSaveTiles;
let baseLayer: TileLayerOffline;
beforeEach(() => {
const leafletMap = new Map(document.createElement('div'));
leafletMap.setView(
{
lat: 51.985,
lng: 5,
},
16,
);
baseLayer = new TileLayerOffline(
'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
{
subdomains: 'abc',
},
).addTo(leafletMap);
saveControl = savetiles(baseLayer, {});
saveControl.addTo(leafletMap);
saveControl._rmTiles();
});
it('exists', () => {
assert.ok(savetiles);
});
it('adds button', () => {
const div = saveControl.onAdd();
assert.ok(div);
assert.lengthOf(div.querySelectorAll('a'), 2);
});
it('calculates storagesize', () =>
saveControl.setStorageSize().then((n) => {
assert.equal(n, 0);
}));
it('_saveTiles sets status', () => {
const stub = sinon
.stub(saveControl, '_loadTile')
.returns(Promise.resolve(new Blob()));
const resetstub = sinon.stub(saveControl, '_resetStatus');
saveControl._saveTiles();
assert.isObject(saveControl.status);
assert.isTrue(resetstub.calledOnce);
stub.resetBehavior();
resetstub.resetBehavior();
});
it('_saveTiles fires savestart with _tilesforSave prop', (done) => {
const stub = sinon
.stub(saveControl, '_loadTile')
.returns(Promise.resolve(new Blob()));
baseLayer.on('savestart', (status) => {
// TODO
// @ts-ignore
assert.lengthOf(status._tilesforSave, 1);
stub.resetBehavior();
done();
});
saveControl._saveTiles();
});
it('_saveTiles calls loadTile for each tile', () => {
const stub = sinon
.stub(saveControl, '_loadTile')
.returns(Promise.resolve(new Blob()));
saveControl._saveTiles();
assert.equal(
stub.callCount,
1,
`_loadTile has been called ${stub.callCount} times`,
);
stub.resetBehavior();
});
});
describe('control with different options', () => {
let leafletMap: Map;
let baseLayer: TileLayerOffline;
beforeEach(() => {
leafletMap = new Map(document.createElement('div'));
leafletMap.setView(
{
lat: 51.985,
lng: 5,
},
16,
);
baseLayer = new TileLayerOffline(
'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
{
subdomains: 'abc',
},
).addTo(leafletMap);
});
it('_saveTiles calculates tiles for 2 zoomlevels', () => {
const c = savetiles(baseLayer, {
zoomlevels: [16, 17],
});
c.addTo(leafletMap);
c._rmTiles();
const stub = sinon
.stub(c, '_loadTile')
.returns(Promise.resolve(new Blob()));
c._saveTiles();
assert.isObject(c.status);
assert.isArray(c.status._tilesforSave);
assert.isAbove(stub.callCount, 1);
stub.resetBehavior();
});
it('_saveTiles calcs tiles for saveWhatYouSee', () => {
const c = savetiles(baseLayer, {
saveWhatYouSee: true,
});
c.addTo(leafletMap);
c._rmTiles();
const stub = sinon
.stub(c, '_loadTile')
.returns(Promise.resolve(new Blob()));
c._saveTiles();
assert.isObject(c.status);
assert.isArray(c.status._tilesforSave);
assert.equal(
stub.callCount,
4,
`_loadTile has been called ${stub.callCount} times`,
);
stub.resetBehavior();
});
it('calls confirm', () => {
const callback = sinon.spy();
const c = savetiles(baseLayer, {
confirm: callback,
});
c.addTo(leafletMap);
c._rmTiles();
c._saveTiles();
assert(callback.calledOnce);
});
});

136
pwa/node_modules/leaflet.offline/test/TileLayerTest.ts generated vendored Normal file
View file

@ -0,0 +1,136 @@
import { Bounds, Point } from 'leaflet';
import { assert } from 'chai';
import { TileLayerOffline } from '../src/TileLayerOffline';
describe('TileLayer.Offline', () => {
it('createTile', () => {
const url = 'http://a.tile.openstreetmap.org/{z}/{x}/{y}.png';
const layer = new TileLayerOffline(url);
// @ts-ignore
const tile = layer.createTile({ x: 123456, y: 456789, z: 16 }, () => {});
assert.instanceOf(tile, HTMLElement);
});
it('get storagekey openstreetmap', () => {
const layer = new TileLayerOffline(
'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
);
const key = layer._getStorageKey({ z: 16, x: 123456, y: 456789 });
assert.equal(key, 'http://a.tile.openstreetmap.org/16/123456/456789.png');
});
it('get storagekey cartodb', () => {
const layer = new TileLayerOffline(
'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png',
);
const key = layer._getStorageKey({ z: 16, x: 123456, y: 456789 });
assert.equal(
key,
'https://cartodb-basemaps-a.global.ssl.fastly.net/light_all/16/123456/456789.png',
);
});
it('get storagekey mapbox with accessToken', () => {
const layer = new TileLayerOffline(
'https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}',
{
id: 'mapbox.streets',
accessToken: 'xyz',
},
);
const key = layer._getStorageKey({ z: 16, x: 123456, y: 456789 });
assert.equal(
key,
'https://api.tiles.mapbox.com/v4/mapbox.streets/16/123456/456789.png?access_token=xyz',
);
});
it('calculates tiles at level 16', () => {
const layer = new TileLayerOffline(
'http://a.tile.openstreetmap.org/{z}/{x}/{y}.png',
);
const bounds = new Bounds(
new Point(8621975, 5543267.999999999),
new Point(8621275, 5542538),
);
const tiles = layer.getTileUrls(bounds, 16);
assert.lengthOf(tiles, 16);
const urls = tiles.map((t) => t.url);
assert.include(urls, 'http://a.tile.openstreetmap.org/16/33677/21651.png');
const keys = tiles.map((t) => t.key);
assert.include(keys, 'http://a.tile.openstreetmap.org/16/33677/21651.png');
});
it('calculates tile urls,keys at level 16 with subdomains', () => {
const layer = new TileLayerOffline(
'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
);
const bounds = new Bounds(
new Point(8621975, 5543267.999999999),
new Point(8621275, 5542538),
);
const tiles = layer.getTileUrls(bounds, 16);
assert.lengthOf(tiles, 16);
const urls = tiles.map((t) => t.url.replace(/[abc]\./, ''));
assert.include(urls, 'http://tile.openstreetmap.org/16/33677/21651.png');
const keys = tiles.map((t) => t.key);
assert.include(keys, 'http://a.tile.openstreetmap.org/16/33677/21651.png');
});
it('uses subdomains for url and not for key', () => {
const layer = new TileLayerOffline(
'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
);
const bounds = new Bounds(
new Point(8621975, 5543267.999999999),
new Point(8621275, 5542538),
);
const tiles = layer.getTileUrls(bounds, 16);
const subs = tiles.map((t) => t.url.match(/([abc])\./)?.[1]);
assert.include(subs, 'a');
assert.include(subs, 'b');
assert.include(subs, 'c');
const subskeys = tiles.map((t) => t.key.match(/([abc])\./)?.[1]);
assert.include(subskeys, 'a');
assert.notInclude(subskeys, 'b');
assert.notInclude(subskeys, 'c');
});
it('calculates openstreetmap tiles at level 16', () => {
const layer = new TileLayerOffline(
'http://a.tile.openstreetmap.org/{z}/{x}/{y}.png',
);
const bounds = new Bounds(
new Point(8621975, 5543267.999999999),
new Point(8621275, 5542538),
);
const tiles = layer.getTileUrls(bounds, 16);
assert.lengthOf(tiles, 16);
const urls = tiles.map((t) => t.url);
assert.include(urls, 'http://a.tile.openstreetmap.org/16/33677/21651.png');
const keys = tiles.map((t) => t.key);
assert.include(keys, 'http://a.tile.openstreetmap.org/16/33677/21651.png');
});
it('calculates mobox tiles at level 16', () => {
const layer = new TileLayerOffline(
'https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}',
{
id: 'mapbox.streets',
accessToken: 'xyz',
},
);
const bounds = new Bounds(
new Point(8621975, 5543267.999999999),
new Point(8621275, 5542538),
);
const tiles = layer.getTileUrls(bounds, 16);
assert.lengthOf(tiles, 16);
const urls = tiles.map((t) => t.url);
assert.include(
urls,
'https://api.tiles.mapbox.com/v4/mapbox.streets/16/33677/21651.png?access_token=xyz',
);
const keys = tiles.map((t) => t.key);
assert.include(
keys,
'https://api.tiles.mapbox.com/v4/mapbox.streets/16/33677/21651.png?access_token=xyz',
);
});
});

View file

@ -0,0 +1,140 @@
/* global describe, it, beforeEach */
import { point, bounds, gridLayer } from 'leaflet';
import fetchMock from 'fetch-mock';
import { assert } from 'chai';
import {
downloadTile,
getStorageInfo,
getStorageLength,
getStoredTilesAsJson,
getTileImageSource,
getTilePoints,
hasTile,
removeTile,
saveTile,
truncate,
} from '../src/TileManager';
const testTileInfo = {
url: 'https://api.tiles.mapbox.com/v4/mapbox.streets/16/33677/21651.png?access_token=xyz',
key: 'https://api.tiles.mapbox.com/v4/mapbox.streets/16/33677/21651.png?access_token=xyz',
x: 33677,
y: 21651,
z: 16,
urlTemplate:
'https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}',
createdAt: Date.now(),
};
describe('manage tile storage', () => {
before(() => {
fetchMock.mockGlobal();
});
after(() => {
fetchMock.unmockGlobal();
});
beforeEach(() => truncate());
it('saves a tile', () =>
saveTile(testTileInfo, new Blob()).then((r) => {
assert.equal(
r,
'https://api.tiles.mapbox.com/v4/mapbox.streets/16/33677/21651.png?access_token=xyz',
);
}));
it('will return empty storageinfo when no tiles are stored', async () => {
const info = await getStorageInfo(testTileInfo.urlTemplate);
assert.lengthOf(info, 0);
});
it('will return storageinfo with single saved tile', async () => {
await saveTile(testTileInfo, new Blob());
const info = await getStorageInfo(testTileInfo.urlTemplate);
assert.lengthOf(info, 1);
const { blob, ...expectedInfo } = info[0];
assert.deepEqual(expectedInfo, testTileInfo);
});
it('will return empty storageinfo for other url template', async () => {
await saveTile(testTileInfo, new Blob());
const info = await getStorageInfo(
'http://someotherexample/{z}/{x}/{y}.png',
);
assert.lengthOf(info, 0);
});
it('will return length 0 on an empty db', async () => {
const length = await getStorageLength();
assert.equal(length, 0);
});
it('will calc tile points', () => {
const minBound = point(0, 0);
const maxBound = point(200, 200);
const tilebounds = bounds(minBound, maxBound);
const tilePoints = getTilePoints(tilebounds, point(256, 256));
assert.lengthOf(tilePoints, 1);
});
it('has tile finds tile by key', async () => {
await saveTile(testTileInfo, new Blob());
const result = await hasTile(testTileInfo.key);
assert.isTrue(result);
});
it('deletes tile finds tile by key', async () => {
await saveTile(testTileInfo, new Blob());
await removeTile(testTileInfo.key);
const result = await hasTile(testTileInfo.key);
assert.isFalse(result);
});
it('Creates geojson with tiles', () => {
const layer = gridLayer();
const json = getStoredTilesAsJson(layer.getTileSize(), [testTileInfo]);
assert.lengthOf(json.features, 1);
const feature = json.features[0];
assert.equal(feature.type, 'Feature');
assert.equal(feature.geometry.type, 'Polygon');
assert.lengthOf(feature.geometry.coordinates, 1);
assert.lengthOf(feature.geometry.coordinates[0], 5);
});
it('downloads a tile', async () => {
const url = 'https://tile.openstreetmap.org/16/33700/21621.png';
fetchMock.once(url, new Blob());
const result = await downloadTile(url);
assert.instanceOf(result, Blob);
fetchMock.removeRoutes();
});
it('downloading a tile throws if response is not successful', async () => {
const url = 'https://tile.openstreetmap.org/16/33700/21621.png';
let err;
fetchMock.once(url, 400);
try {
await downloadTile(url);
} catch (error) {
err = error;
}
assert.instanceOf(err, Error);
fetchMock.removeRoutes();
});
it('get image src returns url if tile with key does not exist', async () => {
const result = await getTileImageSource(testTileInfo.key, testTileInfo.url);
assert.equal(result, testTileInfo.url);
});
it('get image src returns dataSource url if tile key does exist', async () => {
await saveTile(testTileInfo, new Blob());
const result = await getTileImageSource(
testTileInfo.key,
'http://someurl/tile.png',
);
assert.isString(result);
assert.isTrue(result.includes('blob:'));
});
});

12
pwa/node_modules/leaflet.offline/tsconfig.json generated vendored Normal file
View file

@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "es6",
"module": "esnext",
"strict": true,
"moduleResolution":"bundler",
"declaration": true,
"declarationDir": "dist/types",
},
"include": ["./src/**/*","./test/**/*"]
}