chiitiler 0.0.1 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2024 Kanahiro Iguchi
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,27 +1,241 @@
1
- # chiitiler - Tiny VectorTile rendering server
1
+ # chiitiler - The Tiny MapLibre Server
2
2
 
3
- chii-tiler
3
+ ![GitHub Release](https://img.shields.io/github/v/release/Kanahiro/chiitiler?label=ghcr.io/kanahiro/chiitiler)
4
+ ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/Kanahiro/chiitiler/test:unit.yml?label=unittest)
5
+ ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/Kanahiro/chiitiler/test:integration.yml?label=integrationtest)
6
+ [![codecov](https://codecov.io/gh/Kanahiro/chiitiler/graph/badge.svg?token=9RVLAJG126)](https://codecov.io/gh/Kanahiro/chiitiler)
4
7
 
5
- "tiny" in Japanese is "chiisai", shorten this into "chii"
8
+ ![](./logo.webp)*generated by DALL-E*
9
+
10
+ chii-tiler: "tiny" in Japanese is "chiisai", shorten into "chii"
6
11
 
7
12
  ## motivation
8
13
 
9
14
  - In this type of server, there is a de-facto - [maptiler/tileserver-gl](https://github.com/maptiler/tileserver-gl), but this is too big for me.
10
15
  - I want a server accept style.json-url and respond raster tile, inspired by [developmentseed/titiler](https://github.com/developmentseed/titiler)
11
16
 
12
- ## status
17
+ ## features
18
+
19
+ ### /tiles
20
+
21
+ chiitiler provides you with `/tiles` raster-tile endpoint. Once server launched, you can use like this:
22
+
23
+ ```planetext
24
+ http://localhost:3000/tiles/0/0/0.png?url=https://tile.openstreetmap.jp/styles/osm-bright/style.json
25
+ http://localhost:3000/tiles/0/0/0.webp?margin=100&url=https://tile.openstreetmap.jp/styles/maptiler-toner-en/style.json
26
+ http://localhost:3000/tiles/1/1/1.jpg?tileSize=256&quality=80&url=https://tile.openstreetmap.jp/styles/osm-bright/style.json
27
+ ```
28
+
29
+ ### /clip.png|webp|jpg|jpeg
30
+
31
+ chiitiler provides you with `/clip.png|webp|jpg|jpeg` endpoint. Once server launched, you can use like this:
32
+
33
+ ```planetext
34
+ # default size is 1024, this is longer axis and shorter axis is calculated by aspect ratio
35
+ http://localhost:3000/clip.png?bbox=100,30,150,60&url=https://path/to/style.json
36
+ # specify size
37
+ http://localhost:3000/clip.png?bbox=100,30,150,6&size=512&url=https://path/to/style.json
38
+ # specify quality
39
+ http://localhost:3000/clip.png?bbox=100,30,150,6&size=512&quality=80&url=https://path/to/style.json
40
+ ```
41
+
42
+ #### POST endpoint
43
+
44
+ Each endpoint also supports POST method. You can pass style.json as a body. (then, url parameter is not needed)
13
45
 
14
- - this project is under development and experiment
46
+ ## architecture
47
+
48
+ ```mermaid
49
+ graph LR
50
+ subgraph sources
51
+ direction LR
52
+ A[style.json]
53
+ z/x/y.pbf
54
+ z/x/y.png/webp/jpg
55
+ sprite
56
+ glyphs
57
+ end
58
+
59
+ subgraph chiitiler
60
+ cache
61
+ render
62
+ server
63
+ end
64
+
65
+ sources --> cache --> render --> server --/tiles/z/x/y--> png/webp/jpg
66
+
67
+ cache <--get/set--> memory/file/S3
68
+ ```
15
69
 
16
70
  ## usage
17
71
 
72
+ ### Container Image
73
+
74
+ ```sh
75
+ docker pull ghcr.io/kanahiro/chiitiler
76
+ docker run -p 3000:3000 ghcr.io/kanahiro/chiitiler # -> tile-server
77
+
78
+ # recommended: you can use environment variables
79
+ docker run -p 3000:3000 -d \
80
+ -e CHIITILER_CACHE_METHOD=s3 \
81
+ -e CHIITILER_S3CACHE_BUCKET=bucketname \
82
+ -e CHIITILER_S3_REGION=ap-northeast-1 \
83
+ ghcr.io/kanahiro/chiitiler
84
+
85
+ # you also can pass options
86
+ docker run -p 8000:8000 ghcr.io/kanahiro/chiitiler tile-server -p 8000 -c s3 -s3b bucketname -s3r ap-northeast-1
87
+ ```
88
+
89
+ #### Environment Variables
90
+
91
+ you can pass server options via environment variables
92
+
93
+ | env var | default | description |
94
+ | ---------------------------------- | -------- | ---------------------------------------------- |
95
+ | CHIITILER_PORT | 3000 | port number |
96
+ | CHIITILER_PROCESSES | 1 | num of chiitiler processes. 0 means all-CPUs |
97
+ | CHIITILER_DEBUG | false | debug mode |
98
+ | CHIITILER_CACHE_METHOD | none | cache method, `none`, `memory`, `file` or `s3` |
99
+ | CHIITILER_CACHE_TTL_SEC | 3600 | cache ttl, effect to `memory` and `file` |
100
+ | CHIITILER_MEMORYCACHE_MAXITEMCOUNT | 1000 | max items for memorycache |
101
+ | CHIITILER_FILECACHE_DIR | .cache | filecache directory |
102
+ | CHIITILER_S3CACHE_BUCKET | | s3cache bucket name |
103
+ | CHIITILER_S3_REGION | us-east1 | s3 bucket region for caching/fetching |
104
+ | CHIITILER_S3_ENDPOINT | | s3 endpoint for caching/fetching |
105
+
106
+ ### Local
107
+
108
+ - Node.js v18 or v20
109
+
18
110
  ```sh
19
111
  npm install
20
- npm start
112
+ npm run build
113
+ node dist/main.js tile-server
114
+ # running server: http://localhost:3000
21
115
 
22
- # then server will start
23
- # http://localhost:3000/debug
116
+ # develop
117
+ npm run dev
118
+ # running server: http://localhost:3000
119
+ # debug page: http://localhost:3000/debug
24
120
  ```
25
121
 
122
+ #### options
123
+
124
+ ```sh
125
+ node dist/main.js tile-server -p 8000 -c file -ctl 60 -fcd cachedir -D
126
+ # -p: port number
127
+ # -c: cache method
128
+ # -ctl: cache ttl
129
+ # -fcd: cache directory
130
+ # -D: debug mode
131
+
132
+ node dist/main.js tile-server -c memory -ctl 60 -mci 1000
133
+ # -mci: max cache items
134
+
135
+ node dist/main.js tile-server -c s3 -s3b chiitiler -s3r ap-northeast-1
136
+ # -s3b: S3 bucket name for cache
137
+ # -s3r: S3 bucket region
138
+ # caution: TTL is not supported in S3 cache, please utilize S3 lifecycle policy
139
+ ```
140
+
141
+ #### debug page
142
+
143
+ - in debug mode, you can access debug page: <http://localhost:3000/debug>
26
144
  - You can pass style.json url:
27
- - http://localhost:3000/debug?url=https://tile.openstreetmap.jp/styles/osm-bright/style.json
145
+ - <http://localhost:3000/debug?url=https://tile.openstreetmap.jp/styles/osm-bright/style.json>
146
+
147
+ ## supported protocols in style.json
148
+
149
+ - `http://` or `https://` protocol are used in Style Specification
150
+ - In addition, chiitiler supports following protocols:
151
+ - `s3://` for S3 bucket
152
+ - `file://` for local file
153
+ - `mbtiles://` for local MBTIles files
154
+ - `pmtiles://` form PMTiles, remote or local or s3
155
+ - Only when `http://` and `https://` chiitiler will cache them with a specified method.
156
+
157
+ ### example
158
+
159
+ [./localdata/style.json](./localdata/style.json)
160
+
161
+ ```json
162
+ {
163
+ "version": "8",
164
+ "sources": {
165
+ "dir": {
166
+ "type": "vector",
167
+ "tiles": [
168
+ "file://localdata/tiles/{z}/{x}/{y}.pbf"
169
+ ],
170
+ "maxzoom": 6
171
+ },
172
+ "mbtiles": {
173
+ "type": "vector",
174
+ "tiles": [
175
+ "mbtiles://localdata/school.mbtiles/{z}/{x}/{y}"
176
+ ],
177
+ "maxzoom": 10
178
+ },
179
+ "pmtiles": {
180
+ "type": "vector",
181
+ "tiles": [
182
+ "pmtiles://localdata/school.pmtiles/{z}/{x}/{y}"
183
+ ],
184
+ "maxzoom": 10
185
+ },
186
+ "s3": {
187
+ "type": "vector",
188
+ "tiles": [
189
+ "s3://tiles/{z}/{x}/{y}.pbf"
190
+ ],
191
+ "maxzoom": 6
192
+ }
193
+ },
194
+ "layers": [
195
+ {
196
+ "id": "dir",
197
+ "source": "dir",
198
+ "source-layer": "P2921",
199
+ "type": "circle",
200
+ "paint": {
201
+ "circle-radius": 10,
202
+ "circle-color": "red"
203
+ }
204
+ },
205
+ {
206
+ "id": "mbtiles",
207
+ "source": "mbtiles",
208
+ "source-layer": "P2921",
209
+ "type": "circle",
210
+ "paint": {
211
+ "circle-radius": 7,
212
+ "circle-color": "blue"
213
+ }
214
+ },
215
+ {
216
+ "id": "pmtiles",
217
+ "source": "pmtiles",
218
+ "source-layer": "P2921",
219
+ "type": "circle",
220
+ "paint": {
221
+ "circle-radius": 5,
222
+ "circle-color": "yellow"
223
+ }
224
+ },
225
+ {
226
+ "id": "s3",
227
+ "source": "s3",
228
+ "source-layer": "P2921",
229
+ "type": "circle",
230
+ "paint": {
231
+ "circle-radius": 3,
232
+ "circle-color": "green"
233
+ }
234
+ }
235
+ ]
236
+ }
237
+ ```
238
+
239
+ ## development
240
+
241
+ - run `docker compose up`
package/package.json CHANGED
@@ -1,35 +1,53 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "chiitiler",
4
- "version": "0.0.1",
5
- "description": "",
6
- "main": "dist/server.js",
4
+ "version": "1.12.0",
5
+ "description": "Tiny map rendering server for MapLibre Style Spec",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
7
11
  "scripts": {
8
12
  "build": "tsc",
9
- "start": "tsc && node ./dist/server.js",
10
- "prepare": "npm run build"
11
- },
12
- "bin": {
13
- "chiitiler": "./dist/server.js"
13
+ "dev": "tsx watch src/main.ts tile-server -D",
14
+ "start": "tsc && node ./dist/main.js tile-server",
15
+ "test:unit": "vitest src",
16
+ "test:coverage": "vitest src --coverage --coverage.provider=v8",
17
+ "test:integration": "vitest tests/integration.test.ts",
18
+ "test:benchmark": "vitest bench"
14
19
  },
15
20
  "keywords": [],
16
21
  "author": "Kanahiro Iguchi",
17
22
  "license": "MIT",
18
23
  "devDependencies": {
24
+ "@tsconfig/node20": "^20.1.4",
25
+ "@types/better-sqlite3": "^7.6.10",
26
+ "@types/generic-pool": "^3.8.1",
19
27
  "@types/node": "^20.6.0",
28
+ "@types/object-hash": "^3.0.6",
29
+ "@vitest/coverage-v8": "^1.6.0",
30
+ "image-size": "^1.1.1",
20
31
  "maplibre-gl": "^3.3.1",
21
32
  "ts-node": "^10.9.1",
22
- "typescript": "^5.2.2"
33
+ "tsx": "^4.7.1",
34
+ "typescript": "^5.2.2",
35
+ "vitest": "^1.6.0"
23
36
  },
24
37
  "dependencies": {
38
+ "@aws-sdk/client-s3": "^3.418.0",
25
39
  "@hono/node-server": "^1.1.1",
26
40
  "@mapbox/sphericalmercator": "^1.2.0",
27
41
  "@mapbox/tilebelt": "^1.0.2",
28
- "@maplibre/maplibre-gl-native": "^5.2.0",
29
- "@napi-rs/canvas": "^0.1.44",
30
- "@tsconfig/node18": "^18.2.1",
31
- "fast-png": "^6.2.0",
32
- "hono": "^3.6.0",
42
+ "@maplibre/maplibre-gl-native": "^5.4.0",
43
+ "@maplibre/maplibre-gl-style-spec": "^20.2.0",
44
+ "better-sqlite3": "^9.6.0",
45
+ "commander": "^11.0.0",
46
+ "file-system-cache": "^2.4.4",
47
+ "hono": "^4.3.0",
48
+ "lightning-pool": "^4.2.2",
49
+ "memory-cache-node": "^1.4.0",
50
+ "pmtiles": "^3.0.5",
33
51
  "sharp": "^0.32.5"
34
52
  }
35
53
  }
package/.dockerignore DELETED
@@ -1,3 +0,0 @@
1
- node_modules/
2
- .cache/
3
- dist/
package/Dockerfile DELETED
@@ -1,14 +0,0 @@
1
- FROM maptiler/tileserver-gl
2
-
3
- USER root
4
- WORKDIR /app/
5
- COPY package*.json /app/
6
- RUN npm install
7
- COPY . .
8
-
9
- # Lambda WebAdapter
10
- COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.7.1 /lambda-adapter /opt/extensions/lambda-adapter
11
- ENV PORT=3000
12
- ENV READINESS_CHECK_PATH=/health
13
-
14
- CMD ["npm", "start"]
@@ -1,54 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- function escapeFileName(url) {
4
- return url
5
- .replace(/https?:\/\//, '') // remove protocol
6
- .replace(/\//g, '_') // replace slashes with underscores
7
- .replace(/\?/g, '-') // replace question marks with dashes
8
- .replace(/&/g, '-') // replace ampersands with dashes
9
- .replace(/=/g, '-') // replace equals signs with dashes
10
- .replace(/%/g, '-') // replace percent signs with dashes
11
- .replace(/#/g, '-') // replace hash signs with dashes
12
- .replace(/:/g, '-') // replace colons with dashes
13
- .replace(/\+/g, '-') // replace plus signs with dashes
14
- .replace(/ /g, '-') // replace spaces with dashes
15
- .replace(/</g, '-') // replace less than signs with dashes
16
- .replace(/>/g, '-') // replace greater than signs with dashes
17
- .replace(/\*/g, '-') // replace asterisks with dashes
18
- .replace(/\|/g, '-') // replace vertical bars with dashes
19
- .replace(/"/g, '-') // replace double quotes with dashes
20
- .replace(/'/g, '-') // replace single quotes with dashes
21
- .replace(/\?/g, '-') // replace question marks with dashes
22
- .replace(/\./g, '-') // replace dots with dashes
23
- .replace(/,/g, '-') // replace commas with dashes
24
- .replace(/;/g, '-') // replace semicolons with dashes
25
- .replace(/\\/g, '-'); // replace backslashes with dashes
26
- }
27
- const fileCache = function (options) {
28
- return {
29
- set: async function (key, value) {
30
- // write file
31
- return new Promise((resolve, reject) => {
32
- fs.writeFile(path.join(options.dir, escapeFileName(`${key}`)), value, (err) => {
33
- if (err)
34
- reject(err);
35
- resolve();
36
- });
37
- });
38
- },
39
- get: async function (key) {
40
- // read file
41
- return new Promise((resolve, reject) => {
42
- // check exist
43
- if (!fs.existsSync(path.join(options.dir, escapeFileName(`${key}`))))
44
- return resolve(undefined);
45
- fs.readFile(path.join(options.dir, escapeFileName(`${key}`)), (err, data) => {
46
- if (err)
47
- reject(err);
48
- resolve(data);
49
- });
50
- });
51
- },
52
- };
53
- };
54
- export { fileCache };
@@ -1,24 +0,0 @@
1
- import { memoryCache } from './memory.js';
2
- import { s3Cache } from './s3.js';
3
- import { fileCache } from './file.js';
4
- const getCache = function (strategy, options = {}) {
5
- if (strategy === 'none') {
6
- return {
7
- get: async () => undefined,
8
- set: async () => undefined,
9
- };
10
- }
11
- else if (strategy === 'memory') {
12
- return memoryCache();
13
- }
14
- else if (strategy === 'file') {
15
- return fileCache({ dir: options['file:dir'] ?? './.cache' });
16
- }
17
- else if (strategy === 's3') {
18
- return s3Cache({ bucket: options['s3:bucket'] ?? '' });
19
- }
20
- else {
21
- throw new Error(`Invalid cache strategy: ${strategy}`);
22
- }
23
- };
24
- export { getCache, };
@@ -1,12 +0,0 @@
1
- const MEMORY_CACHE_KVS = {};
2
- const memoryCache = function () {
3
- return {
4
- set: async function (key, value) {
5
- MEMORY_CACHE_KVS[key] = value;
6
- },
7
- get: async function (key) {
8
- return MEMORY_CACHE_KVS[key] ?? undefined;
9
- },
10
- };
11
- };
12
- export { memoryCache };
package/dist/cache/s3.js DELETED
@@ -1,10 +0,0 @@
1
- // TODO: implement
2
- const s3Cache = function () {
3
- return {
4
- set: async function (key, value) { },
5
- get: async function (key) {
6
- return undefined;
7
- },
8
- };
9
- };
10
- export { s3Cache };
package/dist/cache.js DELETED
@@ -1,80 +0,0 @@
1
- // TODO: other cache implementation
2
- const getCache = function (strategy) {
3
- if (strategy === 'none') {
4
- return {
5
- get: async () => undefined,
6
- set: async () => undefined,
7
- };
8
- }
9
- else if (strategy === 'memory') {
10
- return memoryCache;
11
- }
12
- else if (strategy === 'file') {
13
- return fileCache;
14
- }
15
- else {
16
- throw new Error(`Invalid cache strategy: ${strategy}`);
17
- }
18
- };
19
- const MEMORY_CACHE_KVS = {};
20
- const memoryCache = {
21
- set: async function (key, value) {
22
- MEMORY_CACHE_KVS[key] = value;
23
- },
24
- get: async function (key) {
25
- return MEMORY_CACHE_KVS[key] ?? undefined;
26
- },
27
- };
28
- import * as fs from 'fs';
29
- import * as path from 'path';
30
- function escapeFileName(url) {
31
- return url
32
- .replace(/https?:\/\//, '') // remove protocol
33
- .replace(/\//g, '_') // replace slashes with underscores
34
- .replace(/\?/g, '-') // replace question marks with dashes
35
- .replace(/&/g, '-') // replace ampersands with dashes
36
- .replace(/=/g, '-') // replace equals signs with dashes
37
- .replace(/%/g, '-') // replace percent signs with dashes
38
- .replace(/#/g, '-') // replace hash signs with dashes
39
- .replace(/:/g, '-') // replace colons with dashes
40
- .replace(/\+/g, '-') // replace plus signs with dashes
41
- .replace(/ /g, '-') // replace spaces with dashes
42
- .replace(/</g, '-') // replace less than signs with dashes
43
- .replace(/>/g, '-') // replace greater than signs with dashes
44
- .replace(/\*/g, '-') // replace asterisks with dashes
45
- .replace(/\|/g, '-') // replace vertical bars with dashes
46
- .replace(/"/g, '-') // replace double quotes with dashes
47
- .replace(/'/g, '-') // replace single quotes with dashes
48
- .replace(/\?/g, '-') // replace question marks with dashes
49
- .replace(/\./g, '-') // replace dots with dashes
50
- .replace(/,/g, '-') // replace commas with dashes
51
- .replace(/;/g, '-') // replace semicolons with dashes
52
- .replace(/\\/g, '-'); // replace backslashes with dashes
53
- }
54
- const FILE_CACHE_DIR = './.cache';
55
- const fileCache = {
56
- set: async function (key, value) {
57
- // write file
58
- return new Promise((resolve, reject) => {
59
- fs.writeFile(path.join(FILE_CACHE_DIR, escapeFileName(`${key}`)), value, (err) => {
60
- if (err)
61
- reject(err);
62
- resolve();
63
- });
64
- });
65
- },
66
- get: async function (key) {
67
- // read file
68
- return new Promise((resolve, reject) => {
69
- // check exist
70
- if (!fs.existsSync(path.join(FILE_CACHE_DIR, escapeFileName(`${key}`))))
71
- return resolve(undefined);
72
- fs.readFile(path.join(FILE_CACHE_DIR, escapeFileName(`${key}`)), (err, data) => {
73
- if (err)
74
- reject(err);
75
- resolve(data);
76
- });
77
- });
78
- },
79
- };
80
- export { getCache, fileCache };
package/dist/server.js DELETED
@@ -1,139 +0,0 @@
1
- /// <reference lib="dom" />
2
- // for using native fetch in TypeScript
3
- import { Hono } from 'hono';
4
- import { serve } from '@hono/node-server';
5
- import sharp from 'sharp';
6
- import { getRenderer } from './tiling.js';
7
- import { getCache } from './cache/index.js';
8
- const cache = getCache('file');
9
- const hono = new Hono();
10
- hono.get('/health', (c) => {
11
- return c.body('OK', 200);
12
- });
13
- hono.get('/tiles/:z/:x/:y_ext', async (c) => {
14
- // path params
15
- const z = Number(c.req.param('z'));
16
- const x = Number(c.req.param('x'));
17
- let [_y, ext] = c.req.param('y_ext').split('.');
18
- const y = Number(_y);
19
- if (['png', 'jpg', 'webp'].indexOf(ext) === -1) {
20
- return c.body('Invalid extension', 400);
21
- }
22
- // query params
23
- const tileSize = Number(c.req.query('tileSize') ?? 512);
24
- const noSymbol = c.req.query('noSymbol') ?? false; // rendering symbol is very slow so provide option to disable it
25
- const onlySymbol = c.req.query('onlySymbol') ?? false;
26
- const url = c.req.query('url') ?? null;
27
- if (url === null) {
28
- return c.body('url is required', 400);
29
- }
30
- // load style.json
31
- let style;
32
- const cachedStyle = await cache.get(url);
33
- if (cachedStyle === undefined) {
34
- const res = await fetch(url);
35
- style = await res.json();
36
- cache.set(url, Buffer.from(JSON.stringify(style)));
37
- }
38
- else {
39
- style = (await JSON.parse(cachedStyle.toString()));
40
- }
41
- if (noSymbol) {
42
- style = {
43
- ...style,
44
- layers: style.layers.filter((layer) => layer.type !== 'symbol'),
45
- };
46
- }
47
- else if (onlySymbol) {
48
- style = {
49
- ...style,
50
- layers: style.layers.filter((layer) => layer.type === 'symbol'),
51
- };
52
- }
53
- const { render } = getRenderer(style, { tileSize });
54
- const pixels = await render(z, x, y);
55
- const image = sharp(pixels, {
56
- raw: {
57
- width: tileSize,
58
- height: tileSize,
59
- channels: 4,
60
- },
61
- });
62
- let imgBuf;
63
- if (ext === 'jpg') {
64
- imgBuf = await image.jpeg({ quality: 20 }).toBuffer();
65
- }
66
- else if (ext === 'webp') {
67
- imgBuf = await image.webp({ quality: 100 }).toBuffer();
68
- }
69
- else {
70
- imgBuf = await image.png().toBuffer();
71
- }
72
- return c.body(imgBuf, 200, {
73
- 'Content-Type': `image/${ext}`,
74
- });
75
- });
76
- hono.get('/debug', (c) => {
77
- //demo tile
78
- const url = c.req.query('url') ?? 'https://demotiles.maplibre.org/style.json';
79
- return c.html(`<!-- show tile in MapLibre GL JS-->
80
-
81
- <!DOCTYPE html>
82
- <html>
83
- <head>
84
- <meta charset="utf-8" />
85
- <title>MapLibre GL JS</title>
86
- <!-- maplibre gl js-->
87
- <script src="https://unpkg.com/maplibre-gl@3.3.1/dist/maplibre-gl.js"></script>
88
- <link
89
- rel="stylesheet"
90
- href="https://unpkg.com/maplibre-gl@3.3.1/dist/maplibre-gl.css"
91
- />
92
- <style>
93
- body {
94
- margin: 0;
95
- padding: 0;
96
- }
97
- #map {
98
- position: absolute;
99
- top: 0;
100
- bottom: 0;
101
- width: 100%;
102
- }
103
- </style>
104
- </head>
105
- <body>
106
- <div id="map" style="height: 100vh"></div>
107
- <script>
108
- const map = new maplibregl.Map({
109
- hash: true,
110
- container: 'map', // container id
111
- style: {
112
- version: 8,
113
- sources: {
114
- chiitiler: {
115
- type: 'raster',
116
- tiles: [
117
- 'http://localhost:3000/tiles/{z}/{x}/{y}.png?url=${url}',
118
- ],
119
- }
120
- },
121
- layers: [
122
- {
123
- id: 'chiitiler',
124
- type: 'raster',
125
- source: 'chiitiler',
126
- minzoom: 0,
127
- maxzoom: 22,
128
- }
129
- ],
130
- },
131
- center: [0, 0], // starting position [lng, lat]
132
- zoom: 1, // starting zoom
133
- });
134
- </script>
135
- </body>
136
- </html>
137
- `);
138
- });
139
- serve({ port: 3000, fetch: hono.fetch });
package/dist/tiling.js DELETED
@@ -1,92 +0,0 @@
1
- /// <reference lib="dom" />
2
- // for using native fetch in TypeScript
3
- import mbgl from '@maplibre/maplibre-gl-native';
4
- // @ts-ignore
5
- import SphericalMercator from '@mapbox/sphericalmercator';
6
- import { getCache } from './cache/index.js';
7
- const cache = getCache('file');
8
- function getTileCenter(z, x, y, tileSize = 256) {
9
- const mercator = new SphericalMercator({
10
- size: tileSize,
11
- });
12
- const px = tileSize / 2 + x * tileSize;
13
- const py = tileSize / 2 + y * tileSize;
14
- const tileCenter = mercator.ll([px, py], z);
15
- return tileCenter;
16
- }
17
- function getRenderer(style, options = { tileSize: 256 }) {
18
- const render = function (z, x, y) {
19
- /**
20
- * zoom(renderingOptions): tileSize=256 -> z-1, 512 -> z, 1024 -> z+1...
21
- * width, height(renderingOptions): equal to tileSize but:
22
- * when zoom=0, entire globe is rendered in 512x512
23
- * even when tilesize=256, entire globe is rendered in "512x512 at zoom=0"
24
- * so we have to set 512 when tilesize=256 and zoom=0, and adjust ratio
25
- */
26
- const renderingParams = options.tileSize === 256 && z === 0
27
- ? {
28
- zoom: 0,
29
- height: 512,
30
- width: 512,
31
- ratio: 0.5,
32
- }
33
- : {
34
- zoom: z - 1 + Math.floor(options.tileSize / 512),
35
- height: options.tileSize,
36
- width: options.tileSize,
37
- ratio: 1,
38
- };
39
- const map = new mbgl.Map({
40
- request: function (req, callback) {
41
- // TODO: better Caching
42
- cache.get(req.url).then((val) => {
43
- if (val !== undefined) {
44
- // hit
45
- callback(undefined, { data: val });
46
- }
47
- else {
48
- fetch(req.url)
49
- .then((res) => {
50
- if (res.status === 200) {
51
- res.arrayBuffer().then((data) => {
52
- cache.set(req.url, Buffer.from(data));
53
- callback(undefined, {
54
- data: Buffer.from(data),
55
- });
56
- });
57
- }
58
- else {
59
- // empty
60
- callback();
61
- }
62
- })
63
- .catch((err) => {
64
- callback(err);
65
- });
66
- }
67
- });
68
- },
69
- ratio: renderingParams.ratio,
70
- mode: "tile" /* MapMode.Tile */,
71
- });
72
- map.load(style);
73
- const renderOptions = {
74
- zoom: renderingParams.zoom,
75
- width: renderingParams.width,
76
- height: renderingParams.height,
77
- center: getTileCenter(z, x, y, options.tileSize),
78
- };
79
- return new Promise((resolve, reject) => {
80
- map.render(renderOptions, function (err, buffer) {
81
- if (err)
82
- reject(err);
83
- resolve(buffer);
84
- map.release();
85
- });
86
- });
87
- };
88
- return {
89
- render,
90
- };
91
- }
92
- export { getRenderer };
package/src/cache/file.ts DELETED
@@ -1,71 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import { type Cache, type Value } from './index.js';
4
-
5
- function escapeFileName(url: string) {
6
- return url
7
- .replace(/https?:\/\//, '') // remove protocol
8
- .replace(/\//g, '_') // replace slashes with underscores
9
- .replace(/\?/g, '-') // replace question marks with dashes
10
- .replace(/&/g, '-') // replace ampersands with dashes
11
- .replace(/=/g, '-') // replace equals signs with dashes
12
- .replace(/%/g, '-') // replace percent signs with dashes
13
- .replace(/#/g, '-') // replace hash signs with dashes
14
- .replace(/:/g, '-') // replace colons with dashes
15
- .replace(/\+/g, '-') // replace plus signs with dashes
16
- .replace(/ /g, '-') // replace spaces with dashes
17
- .replace(/</g, '-') // replace less than signs with dashes
18
- .replace(/>/g, '-') // replace greater than signs with dashes
19
- .replace(/\*/g, '-') // replace asterisks with dashes
20
- .replace(/\|/g, '-') // replace vertical bars with dashes
21
- .replace(/"/g, '-') // replace double quotes with dashes
22
- .replace(/'/g, '-') // replace single quotes with dashes
23
- .replace(/\?/g, '-') // replace question marks with dashes
24
- .replace(/\./g, '-') // replace dots with dashes
25
- .replace(/,/g, '-') // replace commas with dashes
26
- .replace(/;/g, '-') // replace semicolons with dashes
27
- .replace(/\\/g, '-'); // replace backslashes with dashes
28
- }
29
-
30
- type fileCacheOptions = {
31
- dir: string;
32
- };
33
- const fileCache: (options: fileCacheOptions) => Cache = function (options) {
34
- return {
35
- set: async function (key: string, value: Value) {
36
- // write file
37
- return new Promise((resolve, reject) => {
38
- fs.writeFile(
39
- path.join(options.dir, escapeFileName(`${key}`)),
40
- value,
41
- (err: any) => {
42
- if (err) reject(err);
43
- resolve();
44
- },
45
- );
46
- });
47
- },
48
- get: async function (key: string): Promise<Value | undefined> {
49
- // read file
50
- return new Promise((resolve, reject) => {
51
- // check exist
52
- if (
53
- !fs.existsSync(
54
- path.join(options.dir, escapeFileName(`${key}`)),
55
- )
56
- )
57
- return resolve(undefined);
58
-
59
- fs.readFile(
60
- path.join(options.dir, escapeFileName(`${key}`)),
61
- (err: any, data: Buffer) => {
62
- if (err) reject(err);
63
- resolve(data);
64
- },
65
- );
66
- });
67
- },
68
- };
69
- };
70
-
71
- export { fileCache };
@@ -1,42 +0,0 @@
1
- import { memoryCache } from './memory.js';
2
- import { s3Cache } from './s3.js';
3
- import { fileCache } from './file.js';
4
-
5
- type Value = Buffer;
6
- type Strategy = 'none' | 'memory' | 'file' | 's3';
7
- type GetCacheOptions = {
8
- 's3:bucket'?: string;
9
- 'file:dir'?: string;
10
- };
11
- type Cache = {
12
- get: (key: string) => Promise<Value | undefined>;
13
- set: (key: string, value: Value) => Promise<void>;
14
- };
15
-
16
- const getCache = function (
17
- strategy: Strategy,
18
- options: GetCacheOptions = {},
19
- ): Cache {
20
- if (strategy === 'none') {
21
- return {
22
- get: async () => undefined,
23
- set: async () => undefined,
24
- };
25
- } else if (strategy === 'memory') {
26
- return memoryCache();
27
- } else if (strategy === 'file') {
28
- return fileCache({ dir: options['file:dir'] ?? './.cache' });
29
- } else if (strategy === 's3') {
30
- return s3Cache({ bucket: options['s3:bucket'] ?? '' });
31
- } else {
32
- throw new Error(`Invalid cache strategy: ${strategy}`);
33
- }
34
- };
35
-
36
- export {
37
- getCache,
38
- type Value,
39
- type Cache,
40
- type Strategy,
41
- type GetCacheOptions,
42
- };
@@ -1,15 +0,0 @@
1
- import { type Cache, type Value } from './index.js';
2
-
3
- const MEMORY_CACHE_KVS: Record<string, Value> = {};
4
- const memoryCache: () => Cache = function () {
5
- return {
6
- set: async function (key: string, value: Value) {
7
- MEMORY_CACHE_KVS[key] = value;
8
- },
9
- get: async function (key: string): Promise<Value | undefined> {
10
- return (MEMORY_CACHE_KVS[key] as Value) ?? undefined;
11
- },
12
- };
13
- };
14
-
15
- export { memoryCache };
package/src/cache/s3.ts DELETED
@@ -1,17 +0,0 @@
1
- // TODO: implement
2
-
3
- import { type Cache, type Value } from './index.js';
4
-
5
- type S3CacheOptions = {
6
- bucket: string;
7
- };
8
- const s3Cache: (options: S3CacheOptions) => Cache = function () {
9
- return {
10
- set: async function (key: string, value: Value) {},
11
- get: async function (key: string): Promise<Value | undefined> {
12
- return undefined;
13
- },
14
- };
15
- };
16
-
17
- export { s3Cache };
package/src/server.ts DELETED
@@ -1,156 +0,0 @@
1
- /// <reference lib="dom" />
2
- // for using native fetch in TypeScript
3
-
4
- import { Hono } from 'hono';
5
- import { serve } from '@hono/node-server';
6
- import sharp from 'sharp';
7
- import type { StyleSpecification } from 'maplibre-gl';
8
-
9
- import { getRenderer } from './tiling.js';
10
- import { getCache } from './cache/index.js';
11
- const cache = getCache('file');
12
-
13
- const hono = new Hono();
14
- hono.get('/health', (c) => {
15
- return c.body('OK', 200);
16
- });
17
-
18
- hono.get('/tiles/:z/:x/:y_ext', async (c) => {
19
- // path params
20
- const z = Number(c.req.param('z'));
21
- const x = Number(c.req.param('x'));
22
- let [_y, ext] = c.req.param('y_ext').split('.');
23
- const y = Number(_y);
24
-
25
- if (['png', 'jpg', 'webp'].indexOf(ext) === -1) {
26
- return c.body('Invalid extension', 400);
27
- }
28
-
29
- // query params
30
- const tileSize = Number(c.req.query('tileSize') ?? 512);
31
- const noSymbol = c.req.query('noSymbol') ?? false; // rendering symbol is very slow so provide option to disable it
32
- const onlySymbol = c.req.query('onlySymbol') ?? false;
33
- const url = c.req.query('url') ?? null;
34
-
35
- if (url === null) {
36
- return c.body('url is required', 400);
37
- }
38
-
39
- // load style.json
40
- let style: StyleSpecification;
41
-
42
- const cachedStyle = await cache.get(url);
43
- if (cachedStyle === undefined) {
44
- const res = await fetch(url);
45
- style = await res.json();
46
- cache.set(url, Buffer.from(JSON.stringify(style)));
47
- } else {
48
- style = (await JSON.parse(
49
- cachedStyle.toString(),
50
- )) as StyleSpecification;
51
- }
52
-
53
- if (noSymbol) {
54
- style = {
55
- ...style,
56
- layers: style.layers.filter((layer) => layer.type !== 'symbol'),
57
- };
58
- } else if (onlySymbol) {
59
- style = {
60
- ...style,
61
- layers: style.layers.filter((layer) => layer.type === 'symbol'),
62
- };
63
- }
64
-
65
- const { render } = getRenderer(style, { tileSize });
66
- const pixels = await render(z, x, y);
67
-
68
- const image = sharp(pixels, {
69
- raw: {
70
- width: tileSize,
71
- height: tileSize,
72
- channels: 4,
73
- },
74
- });
75
-
76
- let imgBuf: Buffer;
77
- if (ext === 'jpg') {
78
- imgBuf = await image.jpeg({ quality: 20 }).toBuffer();
79
- } else if (ext === 'webp') {
80
- imgBuf = await image.webp({ quality: 100 }).toBuffer();
81
- } else {
82
- imgBuf = await image.png().toBuffer();
83
- }
84
-
85
- return c.body(imgBuf, 200, {
86
- 'Content-Type': `image/${ext}`,
87
- });
88
- });
89
-
90
- hono.get('/debug', (c) => {
91
- //demo tile
92
- const url =
93
- c.req.query('url') ?? 'https://demotiles.maplibre.org/style.json';
94
-
95
- return c.html(`<!-- show tile in MapLibre GL JS-->
96
-
97
- <!DOCTYPE html>
98
- <html>
99
- <head>
100
- <meta charset="utf-8" />
101
- <title>MapLibre GL JS</title>
102
- <!-- maplibre gl js-->
103
- <script src="https://unpkg.com/maplibre-gl@3.3.1/dist/maplibre-gl.js"></script>
104
- <link
105
- rel="stylesheet"
106
- href="https://unpkg.com/maplibre-gl@3.3.1/dist/maplibre-gl.css"
107
- />
108
- <style>
109
- body {
110
- margin: 0;
111
- padding: 0;
112
- }
113
- #map {
114
- position: absolute;
115
- top: 0;
116
- bottom: 0;
117
- width: 100%;
118
- }
119
- </style>
120
- </head>
121
- <body>
122
- <div id="map" style="height: 100vh"></div>
123
- <script>
124
- const map = new maplibregl.Map({
125
- hash: true,
126
- container: 'map', // container id
127
- style: {
128
- version: 8,
129
- sources: {
130
- chiitiler: {
131
- type: 'raster',
132
- tiles: [
133
- 'http://localhost:3000/tiles/{z}/{x}/{y}.png?url=${url}',
134
- ],
135
- }
136
- },
137
- layers: [
138
- {
139
- id: 'chiitiler',
140
- type: 'raster',
141
- source: 'chiitiler',
142
- minzoom: 0,
143
- maxzoom: 22,
144
- }
145
- ],
146
- },
147
- center: [0, 0], // starting position [lng, lat]
148
- zoom: 1, // starting zoom
149
- });
150
- </script>
151
- </body>
152
- </html>
153
- `);
154
- });
155
-
156
- serve({ port: 3000, fetch: hono.fetch });
package/src/tiling.ts DELETED
@@ -1,123 +0,0 @@
1
- /// <reference lib="dom" />
2
- // for using native fetch in TypeScript
3
-
4
- import mbgl, { MapMode } from '@maplibre/maplibre-gl-native';
5
- // @ts-ignore
6
- import SphericalMercator from '@mapbox/sphericalmercator';
7
- import type { StyleSpecification } from 'maplibre-gl';
8
-
9
- import { getCache } from './cache/index.js';
10
- const cache = getCache('file');
11
-
12
- function getTileCenter(z: number, x: number, y: number, tileSize = 256) {
13
- const mercator = new SphericalMercator({
14
- size: tileSize,
15
- });
16
- const px = tileSize / 2 + x * tileSize;
17
- const py = tileSize / 2 + y * tileSize;
18
- const tileCenter = mercator.ll([px, py], z);
19
- return tileCenter;
20
- }
21
-
22
- type GetRendererOptions = {
23
- tileSize: number;
24
- };
25
-
26
- function getRenderer(
27
- style: StyleSpecification,
28
- options: GetRendererOptions = { tileSize: 256 },
29
- ): {
30
- render: (
31
- z: number,
32
- x: number,
33
- y: number,
34
- ) => Promise<Uint8Array | undefined>;
35
- } {
36
- const render = function (
37
- z: number,
38
- x: number,
39
- y: number,
40
- ): Promise<Uint8Array | undefined> {
41
- /**
42
- * zoom(renderingOptions): tileSize=256 -> z-1, 512 -> z, 1024 -> z+1...
43
- * width, height(renderingOptions): equal to tileSize but:
44
- * when zoom=0, entire globe is rendered in 512x512
45
- * even when tilesize=256, entire globe is rendered in "512x512 at zoom=0"
46
- * so we have to set 512 when tilesize=256 and zoom=0, and adjust ratio
47
- */
48
- const renderingParams =
49
- options.tileSize === 256 && z === 0
50
- ? {
51
- zoom: 0,
52
- height: 512,
53
- width: 512,
54
- ratio: 0.5,
55
- }
56
- : {
57
- zoom: z - 1 + Math.floor(options.tileSize / 512),
58
- height: options.tileSize,
59
- width: options.tileSize,
60
- ratio: 1,
61
- };
62
-
63
- const map = new mbgl.Map({
64
- request: function (req, callback) {
65
- // TODO: better Caching
66
- cache.get(req.url).then((val) => {
67
- if (val !== undefined) {
68
- // hit
69
- callback(undefined, { data: val as Buffer });
70
- } else {
71
- fetch(req.url)
72
- .then((res) => {
73
- if (res.status === 200) {
74
- res.arrayBuffer().then(
75
- (data: ArrayBuffer) => {
76
- cache.set(
77
- req.url,
78
- Buffer.from(data),
79
- );
80
- callback(undefined, {
81
- data: Buffer.from(data),
82
- });
83
- },
84
- );
85
- } else {
86
- // empty
87
- callback();
88
- }
89
- })
90
- .catch((err: any) => {
91
- callback(err);
92
- });
93
- }
94
- });
95
- },
96
- ratio: renderingParams.ratio,
97
- mode: MapMode.Tile,
98
- });
99
-
100
- map.load(style);
101
-
102
- const renderOptions = {
103
- zoom: renderingParams.zoom,
104
- width: renderingParams.width,
105
- height: renderingParams.height,
106
- center: getTileCenter(z, x, y, options.tileSize),
107
- };
108
-
109
- return new Promise((resolve, reject) => {
110
- map.render(renderOptions, function (err, buffer) {
111
- if (err) reject(err);
112
- resolve(buffer);
113
- map.release();
114
- });
115
- });
116
- };
117
-
118
- return {
119
- render,
120
- };
121
- }
122
-
123
- export { getRenderer };
package/tsconfig.json DELETED
@@ -1,11 +0,0 @@
1
- {
2
- "extends": ["@tsconfig/node18/tsconfig.json"],
3
- "compilerOptions": {
4
- "moduleResolution": "NodeNext",
5
- "outDir": "./dist",
6
- "resolveJsonModule": true,
7
- "esModuleInterop": true
8
- },
9
- "include": ["src/**/*"],
10
- "exclude": ["node_modules", "build", "dist"]
11
- }