├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── browser-test └── index.html ├── example ├── index.html └── index.ts ├── karma.conf.js ├── package-lock.json ├── package.json ├── test └── index.js ├── ts ├── cached-tile-layer.spec.ts ├── cached-tile-layer.ts └── index.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (http://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # Typescript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # Webstorm 63 | .idea/ 64 | lib/ 65 | typedoc/ 66 | 67 | bundle.js 68 | dist*.js 69 | 70 | example/index.js 71 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (http://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # Typescript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | .idea/ 63 | typedoc/ 64 | 65 | bundle.js 66 | dist*.js 67 | 68 | example/index.js 69 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - "6" 7 | 8 | cache: 9 | directories: 10 | - node_modules 11 | 12 | before_script: 13 | - npm install 14 | - export DISPLAY=:99.0 15 | - sh -e /etc/init.d/xvfb start 16 | 17 | script: 18 | - npm run travis-test 19 | 20 | after_success: 21 | - "cat coverage/Firefox*/lcov.info | ./node_modules/.bin/coveralls" 22 | 23 | branches: 24 | only: 25 | - develop 26 | - master 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 by Arne Schubert 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, 4 | provided that the above copyright notice and this permission notice appear in all copies. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 7 | WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR 8 | CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF 9 | CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS 10 | SOFTWARE. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YAGA cached Tile-Layer for Leaflet 2 | 3 | [![Build Status](https://travis-ci.org/yagajs/leaflet-cached-tile-layer.svg?branch=develop)](https://travis-ci.org/yagajs/leaflet-cached-tile-layer) 4 | [![Coverage Status](https://coveralls.io/repos/github/yagajs/leaflet-cached-tile-layer/badge.svg?branch=develop)](https://coveralls.io/github/yagajs/leaflet-cached-tile-layer?branch=develop) 5 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fyagajs%2Fleaflet-cached-tile-layer.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fyagajs%2Fleaflet-cached-tile-layer?ref=badge_shield) 6 | 7 | A cached tile-layer for [Leaflet](http://leafletjs.com/) realized with the browsers IndexedDB over 8 | [@yaga/indexed-db-tile-cache](https://www.npmjs.com/package/@yaga/indexed-db-tile-cache). 9 | 10 | ## How to use 11 | 12 | At first you have to install this library with `npm` or `yarn`: 13 | 14 | ```bash 15 | npm install --save @yaga/leaflet-cached-tile-layer 16 | # OR 17 | yarn install --save @yaga/leaflet-cached-tile-layer 18 | ``` 19 | 20 | After that you can import this module into your application with the typical node.js or TypeScript way. 21 | 22 | *keep in mind that you have to use browserify to package the libraries from the node.js environment into your browser 23 | ones, such as `Buffer` or `request`.* 24 | 25 | ### Working with the cached Leaflet tile layer 26 | 27 | #### JavaScript 28 | ```javascript 29 | const CachedTileLayer = require('@yaga/leaflet-cached-tile-layer').CachedTileLayer; 30 | const Map = require('leaflet').Map; 31 | 32 | document.addEventListener('DOMContentLoaded', function() { 33 | const map = new Map('map').setView([51.505, -0.09], 13); 34 | 35 | const leafletCachedTileLayer = new CachedTileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', { 36 | attribution: '© OpenStreetMap contributors', 37 | databaseName: 'tile-cache-data', // optional 38 | databaseVersion: 1, // optional 39 | objectStoreName: 'OSM', // optional 40 | crawlDelay: 500, // optional 41 | maxAge: 1000 * 60 * 60 * 24 * 7 // optional 42 | }).addTo(map); 43 | 44 | // The layer caches itself on tile load. 45 | // You can also seed explicit with: 46 | // - `leafletCachedTileLayer.seedCurrentView();` 47 | // - `leafletCachedTileLayer.seedBBox(/* ... */);` 48 | // 49 | // or clear the cache with: 50 | // - `leafletCachedTileLayer.clearCache();` 51 | }); 52 | 53 | ``` 54 | 55 | #### TypeScript 56 | ```typescript 57 | import { CachedTileLayer, ICachedTileLayerSeedProgress } from "@yaga/leaflet-cached-tile-layer"; 58 | import { Map } from "leaflet"; 59 | 60 | document.addEventListener("DOMContentLoaded", () => { 61 | const map = new Map("map").setView([51.505, -0.09], 13); 62 | 63 | const leafletCachedTileLayer = new CachedTileLayer("http://{s}.tile.osm.org/{z}/{x}/{y}.png", { 64 | attribution: `© OpenStreetMap contributors`, 65 | databaseName: "tile-cache-data", // optional 66 | databaseVersion: 1, // optional 67 | objectStoreName: "OSM", // optional 68 | crawlDelay: 500, // optional 69 | maxAge: 1000 * 60 * 60 * 24 * 7, // optional 70 | }).addTo(map); 71 | 72 | // The layer caches itself on tile load. 73 | // You can also seed explicit with: 74 | // - `leafletCachedTileLayer.seedCurrentView();` 75 | // - `leafletCachedTileLayer.seedBBox(/* ... */);` 76 | // 77 | // or clear the cache with: 78 | // - `leafletCachedTileLayer.clearCache();` 79 | }); 80 | ``` 81 | 82 | *There are more methods available, for further information take a look at the API documentation or the example...* 83 | 84 | ## NPM script tasks 85 | 86 | * `npm test`: Runs the software tests with karma and leaves a coverage report under the folder `coverage`. 87 | * `npm run travis-test`: Runs the software tests optimized for the [Travis-CI](https://travis-ci.org/). 88 | * `npm run browser-test`: Prepares the tests to run directly in your browser. After running this command you have to 89 | open `browser-test/index.html` in your browser of choice. 90 | * `npm run doc`: Creates the API documentation with `typedoc` and places the documentation in the folder `typedoc`. 91 | 92 | ## Contribution 93 | 94 | Make an issue on [GitHub](https://github.com/yagajs/leaflet-cached-tile-layer/), or even better a pull request and try 95 | to fulfill the software tests. 96 | 97 | ## License 98 | 99 | This library is under [ISC License](https://spdx.org/licenses/ISC.html) © by Arne Schubert and the YAGA Development 100 | Team. 101 | 102 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fyagajs%2Fleaflet-cached-tile-layer.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fyagajs%2Fleaflet-cached-tile-layer?ref=badge_large) -------------------------------------------------------------------------------- /browser-test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | YAGA Leaflet cached Tile-Layer | Unit-Tests 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | YAGA | leaflet-cached-tile-layer | Example 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 27 |
28 |
29 |
30 |
31 |
32 |
33 | 34 | This example is taken from the original Leaflet example and 35 | is enhanced with the cached tile layer of 36 | the YAGA Development Team. 37 |
38 | 39 | -------------------------------------------------------------------------------- /example/index.ts: -------------------------------------------------------------------------------- 1 | import { Map } from "leaflet"; 2 | import { marker } from "leaflet"; 3 | import { CachedTileLayer, ICachedTileLayerSeedProgress } from "../lib"; 4 | 5 | document.addEventListener("DOMContentLoaded", () => { 6 | (window as any).map = new Map("map").setView([51.505, -0.09], 13); 7 | 8 | (window as any).leafletCachedTileLayer = new CachedTileLayer("http://{s}.tile.osm.org/{z}/{x}/{y}.png", { 9 | attribution: `© OpenStreetMap contributors`, 10 | }).addTo((window as any).map); 11 | 12 | marker([51.5, -0.09]).addTo((window as any).map) 13 | .bindPopup("A pretty CSS3 popup.
Easily customizable.") 14 | .openPopup(); 15 | 16 | const seedButton = document.createElement("button"); 17 | seedButton.setAttribute("class", "btn btn-primary"); 18 | seedButton.appendChild(document.createTextNode("Seed current view")); 19 | seedButton.addEventListener("click", () => { 20 | const progressDiv = document.createElement("div"); 21 | progressDiv.setAttribute("class", "progress"); 22 | 23 | const progressBarDiv = document.createElement("div"); 24 | progressBarDiv.setAttribute("class", "progress-bar progress-bar-success"); 25 | progressBarDiv.setAttribute("style", "width: 0%;"); 26 | progressDiv.appendChild(progressBarDiv); 27 | document.getElementById("progress-wrapper").appendChild(progressDiv); 28 | 29 | (window as any).leafletCachedTileLayer.seedCurrentView( 30 | undefined, 31 | undefined, 32 | (progress: ICachedTileLayerSeedProgress) => { 33 | const percentage = Math.ceil(((progress.total - progress.remains) / progress.total) * 100); 34 | progressBarDiv.setAttribute("style", `width: ${ percentage }%;`); 35 | if (progress.remains === 0) { 36 | progressBarDiv.appendChild(document.createTextNode("Done...")); 37 | setTimeout(() => { 38 | progressDiv.parentNode.removeChild(progressDiv); 39 | }, 3000); 40 | } 41 | }, 42 | ); 43 | }); 44 | 45 | const purgeButton = document.createElement("button"); 46 | purgeButton.setAttribute("class", "btn btn-danger"); 47 | purgeButton.appendChild(document.createTextNode("Purge cache")); 48 | purgeButton.addEventListener("click", () => { 49 | (window as any).leafletCachedTileLayer.clearCache(); 50 | }); 51 | 52 | document.getElementById("button-wrapper").appendChild(seedButton); 53 | document.getElementById("button-wrapper").appendChild(purgeButton); 54 | }); 55 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sun Aug 06 2017 22:57:04 GMT+0200 (CEST) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['mocha'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'test/bundle.js', 19 | 'test/*.png' 20 | // 'test-tmp/*.js' 21 | ], 22 | 23 | 24 | // list of files to exclude 25 | exclude: [ 26 | ], 27 | 28 | 29 | // preprocess matching files before serving them to the browser 30 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 31 | preprocessors: { 32 | }, 33 | 34 | 35 | // optionally, configure the reporter 36 | coverageReporter: { 37 | reporters: [ 38 | { 39 | type : 'html', 40 | dir : 'coverage/' 41 | }, 42 | { 43 | type : 'lcov', 44 | dir : 'coverage/' 45 | } 46 | ] 47 | }, 48 | // test results reporter to use 49 | // possible values: 'dots', 'progress' 50 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 51 | reporters: ['progress', 'coverage'], 52 | 53 | 54 | // web server port 55 | port: 9876, 56 | 57 | 58 | // enable / disable colors in the output (reporters and logs) 59 | colors: true, 60 | 61 | 62 | // level of logging 63 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 64 | logLevel: config.LOG_INFO, 65 | 66 | 67 | // enable / disable watching file and executing tests whenever any file changes 68 | autoWatch: true, 69 | 70 | 71 | // start these browsers 72 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 73 | browsers: ['Chrome', 'Firefox'], 74 | 75 | 76 | // Continuous Integration mode 77 | // if true, Karma captures browsers, runs the tests and exits 78 | singleRun: true, 79 | 80 | // Concurrency level 81 | // how many browser should be started simultaneous 82 | concurrency: Infinity 83 | }) 84 | }; 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaga/leaflet-cached-tile-layer", 3 | "version": "1.0.0", 4 | "description": "A leaflet tile layer cached with @yaga/indexed-db-tile-cache", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "tslint ts/*.ts && tsc && istanbul instrument lib -o test-tmp && browserify test/index.js -o test/bundle.js && karma start karma.conf.js && rm -Rf test-tmp test/bundle.js", 8 | "travis-test": "tslint ts/*.ts && tsc && istanbul instrument lib -o test-tmp && browserify test/index.js -o test/bundle.js && karma start karma.conf.js --browsers Firefox && rm -Rf test-tmp test/bundle.js", 9 | "browser-test": "tsc; cp -R lib test-tmp && browserify test/index.js -o browser-test/bundle.js && rm -Rf test-tmp", 10 | "doc": "typedoc --out ./typedoc/ --exclude ts/tile-layer.directive.spec.ts --mode file ts/", 11 | "build-example": "tsc && cd example && tsc index.ts && cd .. && browserify example/index.js -o example/bundle.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/yagajs/leaflet-cached-tile-layer.git" 16 | }, 17 | "directories": { 18 | "lib": "lib", 19 | "test": "test", 20 | "doc": "typedoc", 21 | "typescript": "ts" 22 | }, 23 | "keywords": [ 24 | "spatial", 25 | "tile", 26 | "cache", 27 | "store", 28 | "storage", 29 | "browser", 30 | "indexed-db" 31 | ], 32 | "devDependencies": { 33 | "@types/chai": "^4.1.2", 34 | "@types/mocha": "^2.2.48", 35 | "bootstrap": "^3.3.7", 36 | "browserify": "^14.5.0", 37 | "chai": "^4.1.2", 38 | "font-awesome": "^4.7.0", 39 | "istanbul": "^0.4.5", 40 | "karma": "^1.7.1", 41 | "karma-chrome-launcher": "^2.2.0", 42 | "karma-coverage": "^1.1.1", 43 | "karma-firefox-launcher": "^1.1.0", 44 | "karma-mocha": "^1.3.0", 45 | "karma-safari-launcher": "^1.0.0", 46 | "mocha": "^3.5.3", 47 | "tslint": "^5.9.1", 48 | "typedoc": "^0.8.0", 49 | "typescript": "^2.7.2", 50 | "uglify-js": "^3.3.12" 51 | }, 52 | "author": "Arne Schubert ", 53 | "license": "ISC", 54 | "dependencies": { 55 | "@types/leaflet": "^1.2.5", 56 | "@yaga/indexed-db-tile-cache": "^1.0.0", 57 | "@yaga/tile-utils": "^1.0.0", 58 | "leaflet": "^1.3.1" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('../test-tmp/cached-tile-layer.spec'); 2 | 3 | -------------------------------------------------------------------------------- /ts/cached-tile-layer.spec.ts: -------------------------------------------------------------------------------- 1 | import { IndexedDbTileCache } from "@yaga/indexed-db-tile-cache"; 2 | import { IBBox } from "@yaga/tile-utils"; 3 | import { expect } from "chai"; 4 | import { LatLngBounds } from "leaflet"; 5 | import { CachedTileLayer, ICachedTileLayerOptions } from "./index"; 6 | 7 | const TEST_URL_TEMPLATE: string = "http://{s}.example.com/{z}/{x}/{y}.png"; 8 | const TRANSPARENT_PIXEL: string = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42m" + 9 | "NkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="; 10 | 11 | /* tslint:disable:no-empty */ 12 | /* istanbul ignore next */ 13 | function noop() {} 14 | /* tslint:enable */ 15 | 16 | describe("CachedTileLayer", () => { 17 | describe(".createTile", () => { 18 | it("should return an HTML image tag", () => { 19 | const cachedTileLayer = new CachedTileLayer(TEST_URL_TEMPLATE); 20 | cachedTileLayer.instantiateIndexedDbTileCache = () => { 21 | return { 22 | getTileAsDataUrl: () => Promise.resolve(TRANSPARENT_PIXEL), 23 | }; 24 | }; 25 | expect(cachedTileLayer.createTile({x: 1, y: 2}, noop)).to.be.an.instanceOf(HTMLElement); 26 | }); 27 | it("should change the source of the HTML image tag", (done) => { 28 | const cachedTileLayer = new CachedTileLayer(TEST_URL_TEMPLATE); 29 | cachedTileLayer.instantiateIndexedDbTileCache = () => { 30 | return { 31 | getTileAsDataUrl: () => { 32 | return Promise.resolve(TRANSPARENT_PIXEL); 33 | }, 34 | }; 35 | }; 36 | const createdTile: HTMLImageElement = cachedTileLayer.createTile({x: 1, y: 2}, noop) as HTMLImageElement; 37 | setTimeout(() => { 38 | expect(createdTile.src).to.equal(TRANSPARENT_PIXEL); 39 | done(); 40 | }, 10); 41 | 42 | }); 43 | it("should give an error tile", (done) => { 44 | const cachedTileLayer = new CachedTileLayer(TEST_URL_TEMPLATE, { 45 | errorTileUrl: TRANSPARENT_PIXEL, 46 | }); 47 | cachedTileLayer.instantiateIndexedDbTileCache = () => { 48 | return { 49 | getTileAsDataUrl: () => { 50 | return Promise.reject(new Error("No further reason... Just for testing...")); 51 | }, 52 | }; 53 | }; 54 | const createdTile: HTMLImageElement = cachedTileLayer.createTile({x: 1, y: 2}, noop) as HTMLImageElement; 55 | setTimeout(() => { 56 | expect(createdTile.src).to.equal(TRANSPARENT_PIXEL); 57 | done(); 58 | }, 10); 59 | 60 | }); 61 | it("should support the cross-origin event if there is no need for", () => { 62 | const cachedTileLayer = new CachedTileLayer(TEST_URL_TEMPLATE, { 63 | crossOrigin: true, 64 | }); 65 | cachedTileLayer.instantiateIndexedDbTileCache = () => { 66 | return { 67 | getTileAsDataUrl: () => Promise.resolve(TRANSPARENT_PIXEL), 68 | }; 69 | }; 70 | expect((cachedTileLayer.createTile({x: 1, y: 2}, noop) as HTMLImageElement).crossOrigin) 71 | .to.equal("anonymous"); 72 | }); 73 | 74 | }); 75 | describe(".instantiateIndexedDbTileCache", () => { 76 | it("should have the right url template even when calling without options", () => { 77 | const tileCacheApi: IndexedDbTileCache = new CachedTileLayer(TEST_URL_TEMPLATE) 78 | .instantiateIndexedDbTileCache(); 79 | expect(tileCacheApi).to.be.an.instanceOf(IndexedDbTileCache); 80 | expect(tileCacheApi.options.tileUrl).to.equal(TEST_URL_TEMPLATE); 81 | }); 82 | it("should return an instance of IndexedDbTileCache with specific options", () => { 83 | const layerOptions: ICachedTileLayerOptions = { 84 | crawlDelay: 1234, 85 | databaseName: "test-db", 86 | databaseVersion: 1, 87 | errorTileUrl: "error.tile", 88 | maxAge: 54321, 89 | objectStoreName: "test-os", 90 | subdomains: ["z", "x", "y"], 91 | }; 92 | const tileCacheApi: IndexedDbTileCache = new CachedTileLayer(TEST_URL_TEMPLATE, layerOptions) 93 | .instantiateIndexedDbTileCache(); 94 | expect(tileCacheApi).to.be.an.instanceOf(IndexedDbTileCache); 95 | expect(tileCacheApi.options.databaseName).to.equal(layerOptions.databaseName); 96 | expect(tileCacheApi.options.databaseVersion).to.equal(layerOptions.databaseVersion); 97 | expect(tileCacheApi.options.objectStoreName).to.equal(layerOptions.objectStoreName); 98 | expect(tileCacheApi.options.tileUrl).to.equal(TEST_URL_TEMPLATE); 99 | expect(tileCacheApi.options.tileUrlSubDomains).to.equal(layerOptions.subdomains); 100 | expect(tileCacheApi.options.crawlDelay).to.equal(layerOptions.crawlDelay); 101 | expect(tileCacheApi.options.maxAge).to.equal(layerOptions.maxAge); 102 | }); 103 | }); 104 | describe(".seedBBox", () => { 105 | it("should call the seedBBox of the IndexedDbTileCache instance", (done) => { 106 | const cachedTileLayer = new CachedTileLayer(TEST_URL_TEMPLATE); 107 | const testBBox: IBBox = { 108 | maxLat: 1, 109 | maxLng: 1, 110 | minLat: -1, 111 | minLng: -1, 112 | }; 113 | const testLeafletBounds: LatLngBounds = new LatLngBounds([-1, -1], [1, 1]); 114 | cachedTileLayer.instantiateIndexedDbTileCache = () => { 115 | return { 116 | seedBBox: (bbox: IBBox, maxZ: number, minZ: number) => { 117 | expect(bbox).to.deep.equal(testBBox); 118 | expect(maxZ).to.equal(20); 119 | expect(minZ).to.equal(10); 120 | done(); 121 | }, 122 | }; 123 | }; 124 | cachedTileLayer.seedBBox(testLeafletBounds, 20, 10); 125 | }); 126 | it("should call the seedBBox of the IndexedDbTileCache instance with current zoom", (done) => { 127 | const cachedTileLayer = new CachedTileLayer(TEST_URL_TEMPLATE); 128 | const testBBox: IBBox = { 129 | maxLat: 1, 130 | maxLng: 1, 131 | minLat: -1, 132 | minLng: -1, 133 | }; 134 | const testLeafletBounds: LatLngBounds = new LatLngBounds([-1, -1], [1, 1]); 135 | 136 | (cachedTileLayer as any)._map = { 137 | getZoom: () => 11, 138 | }; 139 | 140 | cachedTileLayer.instantiateIndexedDbTileCache = () => { 141 | return { 142 | seedBBox: (bbox: IBBox, maxZ: number, minZ: number) => { 143 | expect(bbox).to.deep.equal(testBBox); 144 | expect(maxZ).to.equal(11); 145 | expect(minZ).to.equal(0); 146 | done(); 147 | }, 148 | }; 149 | }; 150 | cachedTileLayer.seedBBox(testLeafletBounds); 151 | }); 152 | it("should call the callback when event emitter fires", (done) => { 153 | const cachedTileLayer = new CachedTileLayer(TEST_URL_TEMPLATE); 154 | const testLeafletBounds: LatLngBounds = new LatLngBounds([-1, -1], [1, 1]); 155 | 156 | (cachedTileLayer as any)._map = { 157 | getZoom: () => 11, 158 | }; 159 | 160 | cachedTileLayer.instantiateIndexedDbTileCache = () => { 161 | return { 162 | on: (name: string, cb: () => void) => { 163 | expect(name).to.equal("seed-progress"); 164 | expect(cb).to.equal(noop); 165 | done(); 166 | }, 167 | seedBBox: noop, 168 | }; 169 | }; 170 | cachedTileLayer.seedBBox(testLeafletBounds, undefined, undefined, noop); 171 | }); 172 | }); 173 | describe(".seedCurrentView", () => { 174 | it("should call the seedBBox of the IndexedDbTileCache instance with the current bounding box", (done) => { 175 | const cachedTileLayer = new CachedTileLayer(TEST_URL_TEMPLATE); 176 | (cachedTileLayer as any)._map = { 177 | getBounds: () => (new LatLngBounds([1, 2], [4, 3])), 178 | }; 179 | cachedTileLayer.instantiateIndexedDbTileCache = () => { 180 | return { 181 | seedBBox: (bbox: IBBox, maxZ: number, minZ: number) => { 182 | expect(bbox.maxLat).to.equal(4); 183 | expect(bbox.maxLng).to.equal(3); 184 | expect(bbox.minLat).to.equal(1); 185 | expect(bbox.minLng).to.equal(2); 186 | expect(maxZ).to.equal(20); 187 | expect(minZ).to.equal(10); 188 | done(); 189 | }, 190 | }; 191 | }; 192 | cachedTileLayer.seedCurrentView(20, 10); 193 | }); 194 | it( 195 | "should call the seedBBox of the IndexedDbTileCache instance with the current bounding box and zoom level", 196 | (done) => { 197 | const cachedTileLayer = new CachedTileLayer(TEST_URL_TEMPLATE); 198 | (cachedTileLayer as any)._map = { 199 | getBounds: () => (new LatLngBounds([1, 2], [4, 3])), 200 | getZoom: () => 11, 201 | }; 202 | cachedTileLayer.instantiateIndexedDbTileCache = () => { 203 | return { 204 | seedBBox: (bbox: IBBox, maxZ: number, minZ: number) => { 205 | expect(bbox.maxLat).to.equal(4); 206 | expect(bbox.maxLng).to.equal(3); 207 | expect(bbox.minLat).to.equal(1); 208 | expect(bbox.minLng).to.equal(2); 209 | expect(maxZ).to.equal(11); 210 | expect(minZ).to.equal(0); 211 | done(); 212 | }, 213 | }; 214 | }; 215 | cachedTileLayer.seedCurrentView(); 216 | }, 217 | ); 218 | }); 219 | describe(".clearCache", () => { 220 | it("should call the purgeStore of the IndexedDbTileCache instance", (done) => { 221 | const cachedTileLayer = new CachedTileLayer(TEST_URL_TEMPLATE); 222 | cachedTileLayer.instantiateIndexedDbTileCache = () => { 223 | return { 224 | purgeStore: () => { 225 | done(); 226 | }, 227 | }; 228 | }; 229 | cachedTileLayer.clearCache(); 230 | }); 231 | }); 232 | }); 233 | -------------------------------------------------------------------------------- /ts/cached-tile-layer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IIndexedDbTileCacheSeedProgress as ICachedTileLayerSeedProgress, 3 | IndexedDbTileCache, 4 | } from "@yaga/indexed-db-tile-cache"; 5 | import { DomEvent, LatLngBounds, Map, TileLayer, TileLayerOptions, Util } from "leaflet"; 6 | 7 | /** 8 | * Interface for the tile layer options. It is a mixin of the original Leaflet `TileLayerOptions` and the 9 | * `IndexedDbTileCacheOptions` of the YAGA Development Team. 10 | */ 11 | export interface ICachedTileLayerOptions extends TileLayerOptions { 12 | /** 13 | * Name of the database 14 | * 15 | * The default value is equal to the constance DEFAULT_DATABASE_NAME 16 | * @default "tile-cache-data" 17 | */ 18 | databaseName?: string; 19 | /** 20 | * Version of the IndexedDB store. Should not be changed normally! But can provide an "upgradeneeded" event from 21 | * IndexedDB. 22 | * 23 | * The default value is equal to the constance DEFAULT_DATABASE_VERSION 24 | * @default 1 25 | */ 26 | databaseVersion?: number; 27 | /** 28 | * Name of the object-store. Should correspond with the name of the tile server 29 | * 30 | * The default value is equal to the constance DEFAULT_OBJECT_STORE_NAME 31 | * @default "OSM"; 32 | */ 33 | objectStoreName?: string; 34 | /** 35 | * The delay in milliseconds used for not stressing the tile server while seeding. 36 | * 37 | * The default value is equal to the constance DEFAULT_CRAWL_DELAY 38 | * @default 500 39 | */ 40 | crawlDelay?: number; 41 | /** 42 | * The maximum age in milliseconds of a stored tile. 43 | * 44 | * The default value is equal to the constance DEFAULT_MAX_AGE 45 | * @default 1000 * 60 * 60 * 24 * 7 46 | */ 47 | maxAge?: number; 48 | } 49 | 50 | /** 51 | * Original Leaflet `TileLayer` enhanced with the `IndexedDbTileCache` of the YAGA Development Team. 52 | */ 53 | export class CachedTileLayer extends TileLayer { 54 | /** 55 | * Options of Leaflets `TileLayer`enhanced with the options for the `IndexedDbTileCache`. 56 | */ 57 | public options: ICachedTileLayerOptions; 58 | constructor(urlTemplate: string, options?: ICachedTileLayerOptions) { 59 | super(urlTemplate, options); 60 | } 61 | 62 | /** 63 | * Rewritten method that serves the tiles from the `IndexedDbTileCache` 64 | */ 65 | public createTile(coords, done): HTMLElement { 66 | // Rewrite of the original method... 67 | const tile = document.createElement("img"); 68 | 69 | DomEvent.on(tile, "load", Util.bind((this as any)._tileOnLoad, this, done, tile)); 70 | DomEvent.on(tile, "error", Util.bind((this as any)._tileOnError, this, done, tile)); 71 | 72 | if (this.options.crossOrigin) { 73 | tile.crossOrigin = ""; 74 | } 75 | 76 | /* 77 | Alt tag is set to empty string to keep screen readers from reading URL and for compliance reasons 78 | http://www.w3.org/TR/WCAG20-TECHS/H67 79 | */ 80 | tile.alt = ""; 81 | 82 | /* 83 | Set role="presentation" to force screen readers to ignore this 84 | https://www.w3.org/TR/wai-aria/roles#textalternativecomputation 85 | */ 86 | tile.setAttribute("role", "presentation"); 87 | 88 | const tc: IndexedDbTileCache = this.instantiateIndexedDbTileCache(); 89 | tc.getTileAsDataUrl({ 90 | x: coords.x, 91 | y: coords.y, 92 | z: (this as any)._getZoomForUrl(), 93 | }).then((dataUrl: string) => { 94 | tile.src = dataUrl; 95 | }).catch(() => { 96 | tile.src = this.options.errorTileUrl; 97 | }); 98 | 99 | return tile; 100 | } 101 | 102 | /** 103 | * Method that creates an instance of the `IndexedDbTileCache` from the options of this object. 104 | * 105 | * You can use this method to make advances operations on the tile cache. 106 | */ 107 | public instantiateIndexedDbTileCache(): IndexedDbTileCache { 108 | return new IndexedDbTileCache({ 109 | crawlDelay: this.options.crawlDelay, 110 | databaseName: this.options.databaseName, 111 | databaseVersion: this.options.databaseVersion, 112 | maxAge: this.options.maxAge, 113 | objectStoreName: this.options.objectStoreName, 114 | tileUrl: (this as any)._url, 115 | tileUrlSubDomains: this.options.subdomains as string[], 116 | }); 117 | } 118 | 119 | /** 120 | * Seed an area with a Leaflet `LatLngBound` and the given zoom range. 121 | * 122 | * The callback will be called before starting to download a tile and once after it is finished. 123 | * 124 | * The default value for `maxZoom` is the current zoom level of the map and the default value for `minZoom` is 125 | * always `0`. 126 | */ 127 | public seedBBox( 128 | bbox: LatLngBounds, 129 | maxZoom?: number, 130 | minZoom: number = 0, 131 | cb?: (progress: ICachedTileLayerSeedProgress) => void, 132 | ): Promise { 133 | if (maxZoom === undefined) { 134 | maxZoom = ((this as any)._map as Map).getZoom(); 135 | } 136 | const tc: IndexedDbTileCache = this.instantiateIndexedDbTileCache(); 137 | if (cb) { 138 | tc.on("seed-progress", cb); 139 | } 140 | return tc.seedBBox({ 141 | maxLat: bbox.getNorth(), 142 | maxLng: bbox.getEast(), 143 | minLat: bbox.getSouth(), 144 | minLng: bbox.getWest(), 145 | }, maxZoom, minZoom); 146 | } 147 | 148 | /** 149 | * Seeds like `this.seedBBox`, but uses the current map bounds as bounding box. 150 | */ 151 | public seedCurrentView( 152 | maxZoom?: number, 153 | minZoom: number = 0, 154 | cb?: (progress: ICachedTileLayerSeedProgress) => void, 155 | ): Promise { 156 | return this.seedBBox(((this as any)._map as Map).getBounds(), maxZoom, minZoom, cb); 157 | } 158 | 159 | /** 160 | * Clears the whole cache. 161 | */ 162 | public clearCache(): Promise { 163 | const tc: IndexedDbTileCache = this.instantiateIndexedDbTileCache(); 164 | return tc.purgeStore(); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /ts/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cached-tile-layer"; 2 | 3 | export { 4 | IIndexedDbTileCacheSeedProgress as ICachedTileLayerSeedProgress, 5 | IndexedDbTileCache, 6 | } from "@yaga/indexed-db-tile-cache"; 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "noImplicitAny": false, 6 | "sourceMap": true, 7 | "declaration": true, 8 | "outDir": "./lib", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "lib": ["es6", "dom", "es2015.iterable", "es2015.promise"] 12 | }, 13 | "exclude": [ 14 | "node_modules", 15 | "lib", 16 | "test", 17 | "coverage", 18 | "example", 19 | "browser-test" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": {}, 8 | "rulesDirectory": [] 9 | } --------------------------------------------------------------------------------