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 +7 -0
- package/README.md +223 -9
- package/package.json +32 -14
- package/.dockerignore +0 -3
- package/Dockerfile +0 -14
- package/dist/cache/file.js +0 -54
- package/dist/cache/index.js +0 -24
- package/dist/cache/memory.js +0 -12
- package/dist/cache/s3.js +0 -10
- package/dist/cache.js +0 -80
- package/dist/server.js +0 -139
- package/dist/tiling.js +0 -92
- package/src/cache/file.ts +0 -71
- package/src/cache/index.ts +0 -42
- package/src/cache/memory.ts +0 -15
- package/src/cache/s3.ts +0 -17
- package/src/server.ts +0 -156
- package/src/tiling.ts +0 -123
- package/tsconfig.json +0 -11
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
|
|
1
|
+
# chiitiler - The Tiny MapLibre Server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
[](https://codecov.io/gh/Kanahiro/chiitiler)
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
*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
|
-
##
|
|
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
|
-
|
|
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
|
|
112
|
+
npm run build
|
|
113
|
+
node dist/main.js tile-server
|
|
114
|
+
# running server: http://localhost:3000
|
|
21
115
|
|
|
22
|
-
#
|
|
23
|
-
|
|
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
|
-
|
|
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": "
|
|
5
|
-
"description": "",
|
|
6
|
-
"main": "dist/
|
|
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
|
-
"
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
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
|
-
"
|
|
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.
|
|
29
|
-
"@
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
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
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"]
|
package/dist/cache/file.js
DELETED
|
@@ -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 };
|
package/dist/cache/index.js
DELETED
|
@@ -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, };
|
package/dist/cache/memory.js
DELETED
|
@@ -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
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 };
|
package/src/cache/index.ts
DELETED
|
@@ -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
|
-
};
|
package/src/cache/memory.ts
DELETED
|
@@ -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
|
-
}
|