├── test └── index.js ├── ts ├── index.ts ├── consts.ts ├── tile-cache.spec.ts └── tile-cache.ts ├── dist.in ├── tslint.json ├── .travis.yml ├── tsconfig.json ├── browser-test └── index.html ├── LICENSE ├── .npmignore ├── .gitignore ├── package.json ├── karma.conf.js └── README.md /test/index.js: -------------------------------------------------------------------------------- 1 | require('../test-tmp/tile-cache.spec'); 2 | -------------------------------------------------------------------------------- /ts/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./tile-cache"; 2 | export * from "./consts"; 3 | -------------------------------------------------------------------------------- /dist.in: -------------------------------------------------------------------------------- 1 | window.yaga = window.yaga || {}; 2 | window.yaga.tileCache = require("./lib/"); 3 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": {}, 8 | "rulesDirectory": [] 9 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /browser-test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | YAGA Indexed-DB Tile Cache | Unit-Tests 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 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 | -------------------------------------------------------------------------------- /ts/consts.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The default database name 3 | */ 4 | export const DEFAULT_DATABASE_NAME: string = "tile-cache-data"; 5 | /** 6 | * The default object store name of the database 7 | */ 8 | export const DEFAULT_OBJECT_STORE_NAME: string = "OSM"; 9 | /** 10 | * The default tile url (the one from OpenStreetMap) 11 | */ 12 | export const DEFAULT_TILE_URL: string = "http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"; 13 | /** 14 | * The default sub domains 15 | */ 16 | export const DEFAULT_TILE_URL_SUB_DOMAINS: string[] = ["a", "b", "c"]; 17 | /** 18 | * The fallback version of your IndexedDB database 19 | */ 20 | export const DEFAULT_DATABASE_VERSION: number = 1; 21 | /** 22 | * The default delay between downloads during the seeding process 23 | */ 24 | export const DEFAULT_CRAWL_DELAY: number = 500; 25 | /** 26 | * The default maximum age of a cached tile (equals one week) 27 | */ 28 | export const DEFAULT_MAX_AGE: number = 1000 * 60 * 60 * 24 * 7; // one week 29 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaga/indexed-db-tile-cache", 3 | "version": "1.0.0", 4 | "description": "Spatial tile cache that saves its data into the IndexedDB of your browser", 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 --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 | "dist": "set -x && tsc && browserify dist.in -o dist.js && uglifyjs dist.js -o dist.min.js", 11 | "doc": "typedoc --out ./typedoc/ --exclude ts/tile-layer.directive.spec.ts --mode file ts/" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/yagajs/indexed-db-tile-cache.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 | "browserify": "^14.5.0", 36 | "chai": "^4.1.2", 37 | "coveralls": "^3.0.0", 38 | "istanbul": "^0.4.5", 39 | "karma": "^1.7.1", 40 | "karma-chrome-launcher": "^2.2.0", 41 | "karma-coverage": "^1.1.1", 42 | "karma-firefox-launcher": "^1.1.0", 43 | "karma-mocha": "^1.3.0", 44 | "karma-safari-launcher": "^1.0.0", 45 | "mocha": "^3.5.3", 46 | "tslint": "^5.9.1", 47 | "typedoc": "^0.8.0", 48 | "typescript": "^2.7.2", 49 | "uglify-js": "^3.3.12" 50 | }, 51 | "author": "Arne Schubert ", 52 | "license": "ISC", 53 | "dependencies": { 54 | "@types/request": "^2.47.0", 55 | "@yaga/tile-utils": "^1.0.0", 56 | "request": "^2.83.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YAGA IndexedDB Tile Cache 2 | 3 | [![Build Status](https://travis-ci.org/yagajs/indexed-db-tile-cache.svg?branch=develop)](https://travis-ci.org/yagajs/indexed-db-tile-cache) 4 | [![Coverage Status](https://coveralls.io/repos/github/yagajs/indexed-db-tile-cache/badge.svg?branch=develop)](https://coveralls.io/github/yagajs/indexed-db-tile-cache?branch=develop) 5 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fyagajs%2Findexed-db-tile-cache.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fyagajs%2Findexed-db-tile-cache?ref=badge_shield) 6 | 7 | A tile storage and cache that uses the browsers IndexedDB to store the spatial map tiles. 8 | 9 | ## Key features 10 | 11 | * On the fly downloading, storing and serving 12 | * Maximal age and auto upgrading of tiles as long as there is a connection 13 | * Seeding in your browser with a bounding box and a zoom level range 14 | * Possibility to serve tiles as base64 data-url including its content type 15 | * Well tested and documented 16 | * Written in and for TypeScript 17 | 18 | 19 | ## How to use 20 | 21 | At first you have to install this library with `npm` or `yarn`: 22 | 23 | ```bash 24 | npm install --save @yaga/indexed-db-tile-cache 25 | # OR 26 | yarn install --save @yaga/indexed-db-tile-cache 27 | ``` 28 | 29 | After that you can import this module into your application with the typical node.js or TypeScript way. 30 | 31 | *keep in mind that you have to use browserify to package the libraries from the node.js environment into your browser 32 | ones, such as `Buffer` or `request`.* 33 | 34 | ### Working with a tile-cache 35 | 36 | #### JavaScript 37 | ```javascript 38 | const indexedDbTileCache = require('@yaga/indexed-db-tile-cache'); 39 | // if you use the precompiled version you can use it similar, just with this change: 40 | // const indexedDbTileCache = window.yaga.tileCache; 41 | 42 | const options = { 43 | databaseName: "tile-cache-data", // optional 44 | databaseVersion: 1, // optional 45 | objectStoreName: "OSM", // optional 46 | tileUrl: "http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", // optional 47 | tileUrlSubDomains: ["a", "b", "c"], // optional 48 | crawlDelay: 500, // optional 49 | maxAge: 1000 * 60 * 60 * 24 * 7, // optional 50 | }; 51 | 52 | const tileCache = new indexedDbTileCache.IndexedDbTileCache(options); 53 | 54 | // get a tile from cache or download if not available: 55 | tileCache.getTileAsDataUrl({x:0, y: 0, z: 0}).then(function(dataUrl) { 56 | const img = document.createElement("img"); 57 | img.src = dataUrl; 58 | document.body.appendChild(img); 59 | }, function(err) { 60 | console.error(err); 61 | }); 62 | 63 | // seed an area: 64 | tileCache.on("seed-progress", function (progress) { 65 | console.log(progess.remains + ' of ' + progress.total + 'tiles remains...'); 66 | }); 67 | tileCache.seedBBox({ 68 | maxLat: 10, 69 | maxLng: 10, 70 | minLat: 1, 71 | minLng: 1, 72 | }).then(function(duration) { 73 | console.log('Seeding completed in ' + duration + 'ms'); 74 | }, function(err) { 75 | console.error(err); 76 | }); 77 | 78 | ``` 79 | 80 | #### TypeScript 81 | ```typescript 82 | import { 83 | IIndexedDbTileCacheOptions, 84 | IIndexedDbTileCacheSeedProgress, 85 | IndexedDbTileCache, 86 | } from "@yaga/indexed-db-tile-cache"; 87 | 88 | const options: IIndexedDbTileCacheOptions = { 89 | databaseName: "tile-cache-data", // optional 90 | databaseVersion: 1, // optional 91 | objectStoreName: "OSM", // optional 92 | tileUrl: "http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", // optional 93 | tileUrlSubDomains: ["a", "b", "c"], // optional 94 | crawlDelay: 500, // optional 95 | maxAge: 1000 * 60 * 60 * 24 * 7, // optional 96 | }; 97 | 98 | const tileCache = new IndexedDbTileCache(options); 99 | 100 | // get a tile from cache or download if not available: 101 | tileCache.getTileAsDataUrl({x:0, y: 0, z: 0}).then((dataUrl: string) => { 102 | const img = document.createElement("img"); 103 | img.src = dataUrl; 104 | document.body.appendChild(img); 105 | }, (err) => { 106 | console.error(err); 107 | }); 108 | 109 | // seed an area: 110 | tileCache.on("seed-progress", (progress: IIndexedDbTileCacheSeedProgress) => { 111 | console.log(`${ progess.remains } of ${ progress.total } tiles remains...`); 112 | }); 113 | tileCache.seedBBox({ 114 | maxLat: 10, 115 | maxLng: 10, 116 | minLat: 1, 117 | minLng: 1, 118 | }).then((duration: number) => { 119 | console.log(`Seeding completed in ${ duration }ms`); 120 | }, (err) => { 121 | console.error(err); 122 | }); 123 | 124 | ``` 125 | 126 | *There are more methods available, for further information take a look at the API documentation...* 127 | 128 | ### Precompiled version 129 | 130 | If you just want to use this library without having the pros of a module loader, you can also run the npm `dist` task 131 | for a packaged and precompiled version. 132 | 133 | At first run: 134 | 135 | ```bash 136 | npm run dist 137 | ``` 138 | 139 | After that you have a `dist.js` and a `dist.min.js` in this project root folder. Now you can use this library like this: 140 | 141 | ```html 142 | 143 | 144 | 145 | 148 | 149 | 150 | ``` 151 | 152 | ## NPM script tasks 153 | 154 | * `npm test`: Runs the software tests with karma and leaves a coverage report under the folder `coverage`. 155 | * `npm run browser-test`: Prepares the tests to run directly in your browser. After running this command you have to open 156 | `browser-test/index.html` in your browser of choice. 157 | * `npm run dist`: Creates an isolated package (without module loader) and registers the module under 158 | `window.yaga.tileCache.*`. 159 | * `npm run doc`: Creates the API documentation with `typedoc` and places the documentation in the folder `typedoc`. 160 | 161 | ## Contribution 162 | 163 | Make an issue on [GitHub](https://github.com/yagajs/indexed-db-tile-cache/), or even better a pull request and try to 164 | fulfill the software tests. 165 | 166 | ## License 167 | 168 | This library is under [ISC License](https://spdx.org/licenses/ISC.html) © by Arne Schubert and the YAGA Development 169 | Team. 170 | 171 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fyagajs%2Findexed-db-tile-cache.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fyagajs%2Findexed-db-tile-cache?ref=badge_large) -------------------------------------------------------------------------------- /ts/tile-cache.spec.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from "buffer"; 2 | import { expect } from "chai"; 3 | import { IIndexedDbTileCacheEntry, IIndexedDbTileCacheSeedProgress, IndexedDbTileCache } from "./index"; 4 | 5 | describe("IndexedDbTileCache", () => { 6 | it("should create a store", () => { 7 | expect(new IndexedDbTileCache()).to.instanceof(IndexedDbTileCache); 8 | }); 9 | it("should purge the store", (done: MochaDone) => { 10 | new IndexedDbTileCache().purgeStore().then(() => { done(); }, done); 11 | }); 12 | it("should fulfill the default values", () => { 13 | const tileCache = new IndexedDbTileCache(); 14 | expect(tileCache.options.databaseName).to.equal("tile-cache-data"); 15 | expect(tileCache.options.databaseVersion).to.equal(1); 16 | expect(tileCache.options.objectStoreName).to.equal("OSM"); 17 | expect(tileCache.options.tileUrl).to.equal("http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"); 18 | expect(tileCache.options.tileUrlSubDomains).to.deep.equal(["a", "b", "c"]); 19 | expect(tileCache.options.crawlDelay).to.equal(500); 20 | expect(tileCache.options.maxAge).to.equal(1000 * 60 * 60 * 24 * 7); 21 | }); 22 | describe(".downloadTile", () => { 23 | it("should download a tile", (done: MochaDone) => { 24 | const tileCache = new IndexedDbTileCache(); 25 | tileCache.downloadTile({x: 0, y: 0, z: 0}).then((value: IIndexedDbTileCacheEntry) => { 26 | expect(value).to.has.property("contentType"); 27 | expect(value).to.has.property("data"); 28 | expect(value).to.has.property("url"); 29 | expect(value).to.has.property("timestamp"); 30 | 31 | expect(value.contentType).to.equal("image/png"); 32 | expect(value.data).to.be.instanceOf(Uint8Array); 33 | expect(value.url).to.equal("http://{s}.tile.openstreetmap.org/0/0/0.png"); 34 | expect(value.timestamp).to.be.a("number"); 35 | done(); 36 | }, done); 37 | }); 38 | it("should have stored the before downloaded tile", (done: MochaDone) => { 39 | const dbRequest: IDBOpenDBRequest = indexedDB.open("tile-cache-data", 1); 40 | 41 | dbRequest.addEventListener("success", (dbEvent: any) => { 42 | const database: IDBDatabase = dbEvent.target.result; 43 | const tx = database.transaction(["OSM"]) 44 | .objectStore("OSM").get("http://{s}.tile.openstreetmap.org/0/0/0.png"); 45 | 46 | tx.addEventListener("success", (event: any) => { 47 | expect(event.target.result).to.has.property("data"); 48 | expect(event.target.result.data).to.be.instanceOf(Uint8Array); 49 | done(); 50 | }); 51 | }); 52 | }); 53 | }); 54 | describe(".getTileEntry", () => { 55 | it("should get the before downloaded tile", (done: MochaDone) => { 56 | const tileCache = new IndexedDbTileCache(); 57 | tileCache.getTileEntry({x: 0 , y: 0, z: 0}).then((tile: IIndexedDbTileCacheEntry) => { 58 | expect(tile.url).to.equal("http://{s}.tile.openstreetmap.org/0/0/0.png"); 59 | done(); 60 | }, done); 61 | }); 62 | it("should not get a new tile without the download flag", (done: MochaDone) => { 63 | const tileCache = new IndexedDbTileCache(); 64 | tileCache.getTileEntry({x: 0 , y: 0, z: 1}) 65 | .then(/* istanbul ignore next */(/* tile: IIndexedDbTileCacheEntry */) => { 66 | done(new Error("Received a tile")); 67 | }, (err) => { 68 | expect(err.message).to.equal("Unable to find entry"); 69 | done(); 70 | }); 71 | }); 72 | it("should get a new tile with the download flag", (done: MochaDone) => { 73 | const tileCache = new IndexedDbTileCache(); 74 | tileCache.getTileEntry({x: 0 , y: 0, z: 2}, true).then((tile: IIndexedDbTileCacheEntry) => { 75 | expect(tile.url).to.equal("http://{s}.tile.openstreetmap.org/2/0/0.png"); 76 | done(); 77 | }, done); 78 | }); 79 | it("should re-download an outdated tile", (done: MochaDone) => { 80 | const dbRequest: IDBOpenDBRequest = indexedDB.open("tile-cache-data", 1); 81 | 82 | dbRequest.addEventListener("success", (dbEvent: any) => { 83 | const database: IDBDatabase = dbEvent.target.result; 84 | const tx = database.transaction(["OSM"], "readwrite") 85 | .objectStore("OSM").put({ 86 | contentType: "wrong/one", 87 | data: new Uint8Array(0), 88 | timestamp: 123, 89 | url: "http://{s}.tile.openstreetmap.org/0/0/0.png", 90 | } as IIndexedDbTileCacheEntry); 91 | 92 | tx.addEventListener("success", () => { 93 | const tileCache = new IndexedDbTileCache(); 94 | tileCache.getTileEntry({x: 0 , y: 0, z: 0}, true).then((tile: IIndexedDbTileCacheEntry) => { 95 | expect(tile.contentType).to.not.equal("wrong/one"); 96 | expect(tile.contentType).to.equal("image/png"); 97 | expect(tile.timestamp).to.be.not.equal(123); 98 | done(); 99 | }, done); 100 | }); 101 | }); 102 | }); 103 | }); 104 | describe(".createInternalTileUrl", () => { 105 | it("should get an url that still have the sub domain as placeholder", () => { 106 | const tileCache = new IndexedDbTileCache(); 107 | expect(tileCache.createInternalTileUrl({x: 1 , y: 2, z: 3})) 108 | .to.equal("http://{s}.tile.openstreetmap.org/3/1/2.png"); 109 | }); 110 | }); 111 | describe(".createTileUrl", () => { 112 | it("should get an url without any placeholder", () => { 113 | const tileCache = new IndexedDbTileCache(); 114 | expect(tileCache.createTileUrl({x: 1 , y: 2, z: 3})) 115 | .to.be.oneOf([ 116 | "http://a.tile.openstreetmap.org/3/1/2.png", 117 | "http://b.tile.openstreetmap.org/3/1/2.png", 118 | "http://c.tile.openstreetmap.org/3/1/2.png", 119 | ]); 120 | }); 121 | }); 122 | describe(".getTileAsBuffer", () => { 123 | it("should get the already fetched tile as Buffer and Uint8Array", (done: MochaDone) => { 124 | const tileCache = new IndexedDbTileCache(); 125 | tileCache.getTileAsBuffer({x: 0, y: 0, z: 0}).then((buffer) => { 126 | expect(buffer).to.be.instanceOf(Buffer); 127 | expect(buffer).to.be.instanceOf(Uint8Array); 128 | done(); 129 | }, done); 130 | }); 131 | it("should get a tile that was not fetched before as Buffer and Uint8Array", (done: MochaDone) => { 132 | const tileCache = new IndexedDbTileCache(); 133 | tileCache.getTileAsBuffer({x: 10, y: 10, z: 10}).then((buffer) => { 134 | expect(buffer).to.be.instanceOf(Buffer); 135 | expect(buffer).to.be.instanceOf(Uint8Array); 136 | done(); 137 | }, done); 138 | }); 139 | }); 140 | describe(".getTileAsDataUrl", () => { 141 | it("should get the already fetched tile as data-url", (done: MochaDone) => { 142 | const tileCache = new IndexedDbTileCache(); 143 | tileCache.getTileAsDataUrl({x: 0, y: 0, z: 0}).then((url: string) => { 144 | expect(url).to.be.a("string"); 145 | expect(url.substr(0, 22)).to.equal("data:image/png;base64,"); 146 | expect(url.length).to.be.greaterThan(100); 147 | done(); 148 | }, done); 149 | }); 150 | it("should get a tile that was not fetched before as Buffer and Uint8Array", (done: MochaDone) => { 151 | const tileCache = new IndexedDbTileCache(); 152 | tileCache.getTileAsDataUrl({x: 20, y: 20, z: 10}).then((url: string) => { 153 | expect(url).to.be.a("string"); 154 | expect(url.substr(0, 22)).to.equal("data:image/png;base64,"); 155 | expect(url.length).to.be.greaterThan(100); 156 | done(); 157 | }, done); 158 | }); 159 | }); 160 | describe(".seedBBox", () => { 161 | it("should seed a bounding-box", (done: MochaDone) => { 162 | const tileCache = new IndexedDbTileCache(); 163 | tileCache.seedBBox({maxLat: 0, maxLng: 0, minLat: 0, minLng: 0}, 0).then(() => { 164 | done(); 165 | }, done); 166 | }); 167 | it("should emit 'seed-progress' while seeding", (done: MochaDone) => { 168 | const tileCache = new IndexedDbTileCache(); 169 | let expectedEmits: number = 2; 170 | tileCache.on("seed-progress", (progress: IIndexedDbTileCacheSeedProgress) => { 171 | expect(progress).to.has.property("remains"); 172 | expect(progress).to.has.property("total"); 173 | expect(progress.total).to.equal(1); 174 | expectedEmits -= 1; 175 | if (expectedEmits === 0) { 176 | return done(); 177 | } 178 | }); 179 | tileCache.seedBBox({maxLat: 0, maxLng: 0, minLat: 0, minLng: 0}, 0).catch(done); 180 | }); 181 | }); 182 | describe(".purgeStore", () => { 183 | it("should purge the whole store", (done: MochaDone) => { 184 | new IndexedDbTileCache().purgeStore().then(() => { 185 | const dbRequest: IDBOpenDBRequest = indexedDB.open("tile-cache-data", 1); 186 | 187 | dbRequest.addEventListener("success", (dbEvent: any) => { 188 | const database: IDBDatabase = dbEvent.target.result; 189 | const tx = database.transaction(["OSM"]) 190 | .objectStore("OSM").get("http://{s}.tile.openstreetmap.org/0/0/0.png"); 191 | 192 | tx.addEventListener("success", (event: any) => { 193 | /* istanbul ignore else */ 194 | if (event.target.result === undefined) { 195 | return done(); 196 | } 197 | /* istanbul ignore next */ 198 | done(new Error("Found removed store")); 199 | }); 200 | tx.addEventListener("error", /* istanbul ignore next */() => { 201 | done(); 202 | }); 203 | }); 204 | }, done); 205 | }); 206 | }); 207 | }); 208 | -------------------------------------------------------------------------------- /ts/tile-cache.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getListOfTilesInBBox, 3 | IBBox, 4 | ITileCoordinates, 5 | } from "@yaga/tile-utils"; 6 | import { Buffer } from "buffer"; 7 | import { EventEmitter } from "events"; 8 | import * as request from "request"; 9 | import { 10 | DEFAULT_CRAWL_DELAY, 11 | DEFAULT_DATABASE_NAME, 12 | DEFAULT_DATABASE_VERSION, 13 | DEFAULT_MAX_AGE, 14 | DEFAULT_OBJECT_STORE_NAME, 15 | DEFAULT_TILE_URL, 16 | DEFAULT_TILE_URL_SUB_DOMAINS, 17 | } from "./consts"; 18 | 19 | /** 20 | * Interface for the options parameter of the constructor of the IndexedDbTileCache class 21 | */ 22 | export interface IIndexedDbTileCacheOptions { 23 | /** 24 | * Name of the database 25 | * 26 | * The default value is equal to the constance DEFAULT_DATABASE_NAME 27 | * @default "tile-cache-data" 28 | */ 29 | databaseName?: string; 30 | /** 31 | * Version of the IndexedDB store. Should not be changed normally! But can provide an "upgradeneeded" event from 32 | * IndexedDB. 33 | * 34 | * The default value is equal to the constance DEFAULT_DATABASE_VERSION 35 | * @default 1 36 | */ 37 | databaseVersion?: number; 38 | /** 39 | * Name of the object-store. Should correspond with the name of the tile server 40 | * 41 | * The default value is equal to the constance DEFAULT_OBJECT_STORE_NAME 42 | * @default "OSM"; 43 | */ 44 | objectStoreName?: string; 45 | /** 46 | * URL template of the tile server. 47 | * 48 | * The default value is equal to the constance DEFAULT_TILE_URL 49 | * @default "http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" 50 | */ 51 | tileUrl?: string; 52 | /** 53 | * A list of all available sub domains for the URL template. 54 | * 55 | * The default value is equal to the constance DEFAULT_TILE_URL_SUB_DOMAINS 56 | * @default ["a", "b", "c"] 57 | */ 58 | tileUrlSubDomains?: string[]; 59 | /** 60 | * The delay in milliseconds used for not stressing the tile server while seeding. 61 | * 62 | * The default value is equal to the constance DEFAULT_CRAWL_DELAY 63 | * @default 500 64 | */ 65 | crawlDelay?: number; 66 | /** 67 | * The maximum age in milliseconds of a stored tile. 68 | * 69 | * The default value is equal to the constance DEFAULT_MAX_AGE 70 | * @default 1000 * 60 * 60 * 24 * 7 71 | */ 72 | maxAge?: number; 73 | } 74 | 75 | /** 76 | * Interface for an internal IndexedDbTileCacheEntry 77 | */ 78 | export interface IIndexedDbTileCacheEntry { 79 | /** 80 | * URL of the tile excepts its sub-domain value that is still stored as placeholder. 81 | */ 82 | url: string; 83 | /** 84 | * Timestamp of the creation date of the entry 85 | */ 86 | timestamp: number; 87 | /** 88 | * Data stored as Uint8Array enhanced with node.js' Buffer 89 | */ 90 | data: Buffer; 91 | /** 92 | * The content-type from the response header. 93 | */ 94 | contentType: string; 95 | } 96 | 97 | /** 98 | * Interface for the "seed-progress" event 99 | */ 100 | export interface IIndexedDbTileCacheSeedProgress { 101 | total: number; 102 | remains: number; 103 | } 104 | 105 | /** 106 | * Class for a spatial-tile-cache that stores its data in the browsers IndexedDB 107 | */ 108 | export class IndexedDbTileCache extends EventEmitter { 109 | constructor(public options: IIndexedDbTileCacheOptions = {}) { 110 | super(); 111 | this.options.databaseName = this.options.databaseName || DEFAULT_DATABASE_NAME; 112 | this.options.databaseVersion = this.options.databaseVersion || DEFAULT_DATABASE_VERSION; 113 | this.options.objectStoreName = this.options.objectStoreName || DEFAULT_OBJECT_STORE_NAME; 114 | this.options.tileUrl = this.options.tileUrl || DEFAULT_TILE_URL; 115 | this.options.tileUrlSubDomains = this.options.tileUrlSubDomains || DEFAULT_TILE_URL_SUB_DOMAINS; 116 | this.options.crawlDelay = this.options.crawlDelay || DEFAULT_CRAWL_DELAY; 117 | this.options.maxAge = this.options.maxAge || DEFAULT_MAX_AGE; 118 | 119 | // Create the store if it does not exists... 120 | const dbRequest: IDBOpenDBRequest = indexedDB.open(this.options.databaseName, this.options.databaseVersion); 121 | dbRequest.addEventListener("upgradeneeded", (dbEvent: any) => { 122 | /** 123 | * Fired event from IndexedDB to give the possibility to enhance something on the store 124 | * @event IndexedDbTileCache#upgradeneeded 125 | */ 126 | this.emit("upgradeneeded", dbEvent); 127 | const database: IDBDatabase = dbEvent.target.result; 128 | database.createObjectStore(this.options.objectStoreName, { keyPath: "url"}); 129 | }); 130 | dbRequest.addEventListener("error", (dbEvent: any) => { 131 | /** 132 | * Piping the error event 133 | * @event IndexedDbTileCache#upgradeneeded 134 | */ 135 | this.emit("error", dbEvent.target.error); 136 | }); 137 | } 138 | 139 | /** 140 | * Get the internal tile entry from the database with all its additional meta information. 141 | * 142 | * If the tile is marked as outdated by the `IIndexedDbTileCacheOptions.maxAge` property, it tries to download it 143 | * again. On any error it will provide the cached version. 144 | * 145 | * If you pass `true` as parameter for the `downloadIfUnavaiable` argument, it tries to dowenload a tile if it is 146 | * not stored already. 147 | */ 148 | public getTileEntry( 149 | tileCoordinates: ITileCoordinates, 150 | downloadIfUnavaiable?: boolean, 151 | ): Promise { 152 | const dbRequest: IDBOpenDBRequest = indexedDB.open(this.options.databaseName, this.options.databaseVersion); 153 | 154 | return new Promise((resolve, reject) => { 155 | dbRequest.addEventListener("success", (dbEvent: any) => { 156 | const database: IDBDatabase = dbEvent.target.result; 157 | const tx = database.transaction([this.options.objectStoreName]) 158 | .objectStore(this.options.objectStoreName).get(this.createInternalTileUrl(tileCoordinates)); 159 | 160 | tx.addEventListener("success", (event: any) => { 161 | if (!event.target.result) { 162 | if (downloadIfUnavaiable) { 163 | return this.downloadTile(tileCoordinates).then(resolve, reject); 164 | } 165 | return reject(new Error("Unable to find entry")); 166 | } 167 | const tileEntry: IIndexedDbTileCacheEntry = event.target.result as IIndexedDbTileCacheEntry; 168 | // Make a buffer from UInt8Array to get additional methods 169 | if (!(tileEntry.data instanceof Buffer)) { 170 | tileEntry.data = new Buffer(tileEntry.data); 171 | } 172 | 173 | if (tileEntry.timestamp < Date.now() - this.options.maxAge) { // Too old 174 | return this.downloadTile(tileCoordinates).catch(() => { 175 | // Not available so keep cached version... 176 | return resolve(tileEntry as IIndexedDbTileCacheEntry); 177 | }).then(resolve as (value: IIndexedDbTileCacheEntry) => void); 178 | } 179 | resolve(tileEntry); 180 | }); 181 | tx.addEventListener("error", (event: any) => { 182 | this.emit("error", dbEvent.target.error); 183 | reject(event.target.error); 184 | }); 185 | }); 186 | 187 | dbRequest.addEventListener("error", (dbEvent: any) => { 188 | this.emit("error", dbEvent.target.error); 189 | reject(dbEvent.target.error); 190 | }); 191 | }); 192 | } 193 | 194 | /** 195 | * Creates an internal tile url from the url template from IIndexedDbTileCacheOptions 196 | * 197 | * It keeps the sub-domain placeholder to provide unique database entries while seeding from multiple sub-domains. 198 | */ 199 | public createInternalTileUrl(tileCoordinates: ITileCoordinates): string { 200 | return this.options.tileUrl 201 | .split(/{x}/).join(tileCoordinates.x.toString()) 202 | .split(/{y}/).join(tileCoordinates.y.toString()) 203 | .split(/{z}/).join(tileCoordinates.z.toString()); 204 | } 205 | 206 | /** 207 | * Creates a real tile url from the url template from IIndexedDbTileCacheOptions 208 | */ 209 | public createTileUrl(tileCoordinates: ITileCoordinates): string { 210 | const randomSubDomain: string = this.options 211 | .tileUrlSubDomains[Math.floor(Math.random() * this.options.tileUrlSubDomains.length)]; 212 | 213 | return this.createInternalTileUrl(tileCoordinates) 214 | .split(/{s}/).join(randomSubDomain); 215 | } 216 | 217 | /** 218 | * Receive a tile as an Uint8Array / Buffer 219 | */ 220 | public getTileAsBuffer(tileCoordinates: ITileCoordinates): Promise { 221 | return this.getTileEntry(tileCoordinates, true).then((tileEntry: IIndexedDbTileCacheEntry) => { 222 | return Promise.resolve(tileEntry.data); 223 | }); 224 | } 225 | 226 | /** 227 | * Receives a tile as its base64 encoded data url. 228 | */ 229 | public getTileAsDataUrl(tileCoordinates: ITileCoordinates): Promise { 230 | return this.getTileEntry(tileCoordinates, true).then((tileEntry: IIndexedDbTileCacheEntry) => { 231 | return Promise.resolve("data:" + tileEntry.contentType + ";base64," + tileEntry.data.toString("base64")); 232 | }); 233 | } 234 | 235 | /** 236 | * Download a specific tile by its coordinates and store it within the indexed-db 237 | */ 238 | public downloadTile(tileCoordinates: ITileCoordinates): Promise { 239 | const buffers: Buffer[] = []; 240 | return new Promise((resolve, reject) => { 241 | let contentType: string = ""; 242 | request.get(this.createTileUrl(tileCoordinates)) 243 | .on("data", (chunk: Buffer) => { 244 | buffers.push(chunk); 245 | }) 246 | .on("response", (response) => { 247 | contentType = response.headers["content-type"] as string; 248 | }) 249 | .on("error", reject) 250 | .on("end", () => { 251 | const dbRequest: IDBOpenDBRequest = indexedDB.open( 252 | this.options.databaseName, 253 | this.options.databaseVersion, 254 | ); 255 | 256 | const tileCacheEntry: IIndexedDbTileCacheEntry = { 257 | contentType, 258 | data: Buffer.concat(buffers), 259 | timestamp: Date.now(), 260 | url: this.createInternalTileUrl(tileCoordinates), 261 | }; 262 | 263 | dbRequest.addEventListener("success", (dbEvent: any) => { 264 | const database: IDBDatabase = dbEvent.target.result; 265 | const tx = database.transaction([this.options.objectStoreName], "readwrite") 266 | .objectStore(this.options.objectStoreName).put(tileCacheEntry); 267 | 268 | tx.addEventListener("success", () => { 269 | resolve(tileCacheEntry); 270 | }); 271 | tx.addEventListener("error", (event: any) => { 272 | this.emit("error", event.target.error); 273 | reject(event.target.error); 274 | }); 275 | }); 276 | 277 | dbRequest.addEventListener("error", (dbEvent: any) => { 278 | this.emit("error", dbEvent.target.error); 279 | reject(dbEvent.target.error); 280 | }); 281 | }); 282 | }); 283 | } 284 | 285 | /** 286 | * Seeds an area of tiles by the given bounding box, the maximal z value and the optional minimal z value. 287 | * 288 | * The returned number in the promise is equal to the duration of the operation in milliseconds. 289 | */ 290 | public seedBBox(bbox: IBBox, maxZ: number, minZ: number = 0): Promise { 291 | const start = Date.now(); 292 | const list: ITileCoordinates[] = getListOfTilesInBBox(bbox, maxZ, minZ); 293 | const total: number = list.length; 294 | return new Promise((resolve, reject) => { 295 | const fn = () => { 296 | /** 297 | * @event IndexedDbTileCache#seed-progess 298 | * @type IIndexedDbTileCacheSeedProgress 299 | */ 300 | this.emit("seed-progress", {total, remains: list.length} as IIndexedDbTileCacheSeedProgress); 301 | const val: ITileCoordinates = list.shift(); 302 | if (val) { 303 | this.downloadTile(val).then(() => { 304 | setTimeout(fn, this.options.crawlDelay); 305 | }, reject); 306 | return; 307 | } 308 | resolve(Date.now() - start); 309 | }; 310 | fn(); 311 | }); 312 | } 313 | 314 | /** 315 | * Purge the whole store 316 | */ 317 | public purgeStore(): Promise { 318 | const dbRequest: IDBOpenDBRequest = indexedDB.open(this.options.databaseName, this.options.databaseVersion); 319 | 320 | return new Promise((resolve, reject) => { 321 | dbRequest.addEventListener("success", (dbEvent: any) => { 322 | const database: IDBDatabase = dbEvent.target.result; 323 | const tx = database.transaction([this.options.objectStoreName], "readwrite") 324 | .objectStore(this.options.objectStoreName).clear(); 325 | 326 | tx.addEventListener("success", (/* event: any */) => { 327 | resolve(true); 328 | }); 329 | tx.addEventListener("error", (event: any) => { 330 | this.emit("error", dbEvent.target.error); 331 | reject(event.target.error); 332 | }); 333 | }); 334 | 335 | dbRequest.addEventListener("error", (dbEvent: any) => { 336 | this.emit("error", dbEvent.target.error); 337 | }); 338 | }); 339 | } 340 | } 341 | --------------------------------------------------------------------------------