├── 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 | [](https://travis-ci.org/yagajs/indexed-db-tile-cache)
4 | [](https://coveralls.io/github/yagajs/indexed-db-tile-cache?branch=develop)
5 | [](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 | [](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 |
--------------------------------------------------------------------------------