chiitiler 1.12.2 → 1.12.3
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/dist/cache/file.d.ts +7 -0
- package/dist/cache/file.js +33 -0
- package/dist/cache/index.d.ts +12 -0
- package/dist/cache/index.js +9 -0
- package/dist/cache/memory.d.ts +7 -0
- package/dist/cache/memory.js +18 -0
- package/dist/cache/s3.d.ts +8 -0
- package/dist/cache/s3.js +65 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +101 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +22 -0
- package/dist/render/index.d.ts +25 -0
- package/dist/render/index.js +146 -0
- package/dist/render/pool.d.ts +6 -0
- package/dist/render/pool.js +75 -0
- package/dist/render/rasterize.d.ts +10 -0
- package/dist/render/rasterize.js +66 -0
- package/dist/s3.d.ts +6 -0
- package/dist/s3.js +24 -0
- package/dist/server/debug.d.ts +4 -0
- package/dist/server/debug.js +201 -0
- package/dist/server/index.d.ts +15 -0
- package/dist/server/index.js +187 -0
- package/dist/source/fs.d.ts +3 -0
- package/dist/source/fs.js +15 -0
- package/dist/source/http.d.ts +5 -0
- package/dist/source/http.js +25 -0
- package/dist/source/index.d.ts +10 -0
- package/dist/source/index.js +27 -0
- package/dist/source/mbtiles.d.ts +6 -0
- package/dist/source/mbtiles.js +39 -0
- package/dist/source/pmtiles.d.ts +9 -0
- package/dist/source/pmtiles.js +106 -0
- package/dist/source/s3.d.ts +3 -0
- package/dist/source/s3.js +27 -0
- package/package.json +4 -3
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
function getDebugPage(c) {
|
|
2
|
+
//demo tile
|
|
3
|
+
const url = c.req.query('url') ?? 'https://demotiles.maplibre.org/style.json';
|
|
4
|
+
const margin = Number(c.req.query('margin') ?? 0);
|
|
5
|
+
const quality = Number(c.req.query('quality') ?? 100);
|
|
6
|
+
const tileSize = Number(c.req.query('tileSize') ?? 512);
|
|
7
|
+
// show tile in MapLibre GL JS
|
|
8
|
+
return c.html(`<!DOCTYPE html>
|
|
9
|
+
<html>
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="utf-8" />
|
|
12
|
+
<title>MapLibre GL JS</title>
|
|
13
|
+
<!-- maplibre gl js-->
|
|
14
|
+
<script src="https://unpkg.com/maplibre-gl@^4.0/dist/maplibre-gl.js"></script>
|
|
15
|
+
<link
|
|
16
|
+
rel="stylesheet"
|
|
17
|
+
href="https://unpkg.com/maplibre-gl@^4.0/dist/maplibre-gl.css"
|
|
18
|
+
/>
|
|
19
|
+
<style>
|
|
20
|
+
body {
|
|
21
|
+
margin: 0;
|
|
22
|
+
padding: 0;
|
|
23
|
+
}
|
|
24
|
+
#map {
|
|
25
|
+
position: absolute;
|
|
26
|
+
top: 0;
|
|
27
|
+
bottom: 0;
|
|
28
|
+
width: 100%;
|
|
29
|
+
}
|
|
30
|
+
</style>
|
|
31
|
+
</head>
|
|
32
|
+
<body>
|
|
33
|
+
<div id="map" style="height: 100vh"></div>
|
|
34
|
+
<script>
|
|
35
|
+
// hostname
|
|
36
|
+
const tileUrl = window.location.origin + '/tiles/{z}/{x}/{y}.webp?url=${url}&quality=${quality}&margin=${margin}&tileSize=${tileSize}';
|
|
37
|
+
|
|
38
|
+
const map = new maplibregl.Map({
|
|
39
|
+
hash: true,
|
|
40
|
+
container: 'map', // container id
|
|
41
|
+
style: {
|
|
42
|
+
version: 8,
|
|
43
|
+
sources: {
|
|
44
|
+
chiitiler: {
|
|
45
|
+
type: 'raster',
|
|
46
|
+
tiles: [tileUrl],
|
|
47
|
+
tileSize: ${tileSize},
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
layers: [
|
|
51
|
+
{
|
|
52
|
+
id: 'chiitiler',
|
|
53
|
+
type: 'raster',
|
|
54
|
+
source: 'chiitiler',
|
|
55
|
+
minzoom: 0,
|
|
56
|
+
maxzoom: 22,
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
center: [0, 0], // starting position [lng, lat]
|
|
61
|
+
zoom: 1, // starting zoom
|
|
62
|
+
});
|
|
63
|
+
</script>
|
|
64
|
+
</body>
|
|
65
|
+
</html>`);
|
|
66
|
+
}
|
|
67
|
+
function getEditorgPage(c) {
|
|
68
|
+
return c.html(`<!DOCTYPE html>
|
|
69
|
+
<html>
|
|
70
|
+
<head>
|
|
71
|
+
<meta charset="utf-8" />
|
|
72
|
+
<title>MapLibre GL JS</title>
|
|
73
|
+
<!-- maplibre gl js-->
|
|
74
|
+
<script src="https://unpkg.com/maplibre-gl@^4.0/dist/maplibre-gl.js"></script>
|
|
75
|
+
<link
|
|
76
|
+
rel="stylesheet"
|
|
77
|
+
href="https://unpkg.com/maplibre-gl@^4.0/dist/maplibre-gl.css"
|
|
78
|
+
/>
|
|
79
|
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/10.0.3/jsoneditor.css" rel="stylesheet" type="text/css">
|
|
80
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/10.0.3/jsoneditor.min.js"></script>
|
|
81
|
+
<style>
|
|
82
|
+
body {
|
|
83
|
+
margin: 0;
|
|
84
|
+
padding: 0;
|
|
85
|
+
}
|
|
86
|
+
</style>
|
|
87
|
+
</head>
|
|
88
|
+
<body>
|
|
89
|
+
<div id="map" style="height: 50vh"></div>
|
|
90
|
+
<div id="jsoneditor" style="height: 50vh"></div>
|
|
91
|
+
<script>
|
|
92
|
+
const container = document.getElementById("jsoneditor")
|
|
93
|
+
const editor = new JSONEditor(container, {
|
|
94
|
+
mode: 'code',
|
|
95
|
+
onChange: function() {
|
|
96
|
+
reloadStyle()
|
|
97
|
+
localStorage.setItem('style', JSON.stringify(editor.get()))
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const initialStyle = localStorage.getItem('style') ? JSON.parse(localStorage.getItem('style')) : {
|
|
102
|
+
version: 8,
|
|
103
|
+
sources: {
|
|
104
|
+
osm: {
|
|
105
|
+
type: 'raster',
|
|
106
|
+
tiles: ['https://a.tile.openstreetmap.org/{z}/{x}/{y}.png'],
|
|
107
|
+
tileSize: 256,
|
|
108
|
+
},
|
|
109
|
+
point: {
|
|
110
|
+
type: 'geojson',
|
|
111
|
+
data: {
|
|
112
|
+
type: 'FeatureCollection',
|
|
113
|
+
features: [
|
|
114
|
+
{
|
|
115
|
+
type: 'Feature',
|
|
116
|
+
properties: {},
|
|
117
|
+
geometry: {
|
|
118
|
+
type: 'Point',
|
|
119
|
+
coordinates: [140, 40],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
layers: [
|
|
127
|
+
{
|
|
128
|
+
id: 'osm',
|
|
129
|
+
type: 'raster',
|
|
130
|
+
source: 'osm',
|
|
131
|
+
minzoom: 0,
|
|
132
|
+
maxzoom: 22,
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: 'geojson',
|
|
136
|
+
type: 'circle',
|
|
137
|
+
source: 'point',
|
|
138
|
+
paint: {
|
|
139
|
+
'circle-radius': 10,
|
|
140
|
+
'circle-color': '#f00',
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
editor.set(initialStyle);
|
|
147
|
+
|
|
148
|
+
maplibregl.addProtocol('post', async (params, abortController) => {
|
|
149
|
+
const imageUrl = params.url.replace('post://', '');
|
|
150
|
+
const style = editor.get()
|
|
151
|
+
const png = await fetch(imageUrl, {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: {
|
|
154
|
+
'Content-Type': 'application/json',
|
|
155
|
+
},
|
|
156
|
+
body: JSON.stringify({ style }),
|
|
157
|
+
}).then((res) => res.arrayBuffer());
|
|
158
|
+
return { data: png };
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const style = {
|
|
162
|
+
version: 8,
|
|
163
|
+
sources: {
|
|
164
|
+
chiitiler: {
|
|
165
|
+
type: 'raster',
|
|
166
|
+
tiles: ['post://' + window.location.origin + '/tiles/{z}/{x}/{y}.png'],
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
layers: [
|
|
170
|
+
{
|
|
171
|
+
id: 'chiitiler',
|
|
172
|
+
type: 'raster',
|
|
173
|
+
source: 'chiitiler',
|
|
174
|
+
minzoom: 0,
|
|
175
|
+
maxzoom: 22,
|
|
176
|
+
}
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const map = new maplibregl.Map({
|
|
181
|
+
hash: true,
|
|
182
|
+
container: 'map', // container id
|
|
183
|
+
style,
|
|
184
|
+
center: [0, 0], // starting position [lng, lat]
|
|
185
|
+
zoom: 1, // starting zoom
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// reload button
|
|
189
|
+
function reloadStyle() {
|
|
190
|
+
map.setStyle({
|
|
191
|
+
version: 8,
|
|
192
|
+
sources: {},
|
|
193
|
+
layers: [],
|
|
194
|
+
});
|
|
195
|
+
map.setStyle(style);
|
|
196
|
+
}
|
|
197
|
+
</script>
|
|
198
|
+
</body>
|
|
199
|
+
</html>`);
|
|
200
|
+
}
|
|
201
|
+
export { getDebugPage, getEditorgPage };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Hono } from 'hono/quick';
|
|
2
|
+
import { type Cache } from '../cache/index.js';
|
|
3
|
+
type InitServerOptions = {
|
|
4
|
+
cache: Cache;
|
|
5
|
+
port: number;
|
|
6
|
+
debug: boolean;
|
|
7
|
+
};
|
|
8
|
+
type InitializedServer = {
|
|
9
|
+
app: Hono;
|
|
10
|
+
tiles: Hono;
|
|
11
|
+
clip: Hono;
|
|
12
|
+
start: () => void;
|
|
13
|
+
};
|
|
14
|
+
declare function initServer(options: InitServerOptions): InitializedServer;
|
|
15
|
+
export { initServer, type InitServerOptions };
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { Hono } from 'hono/quick';
|
|
2
|
+
import { serve } from '@hono/node-server';
|
|
3
|
+
import { validateStyleMin, } from '@maplibre/maplibre-gl-style-spec';
|
|
4
|
+
import { getDebugPage, getEditorgPage } from './debug.js';
|
|
5
|
+
import { getRenderedTileBuffer, getRenderedBboxBuffer, } from '../render/index.js';
|
|
6
|
+
function isValidStylejson(stylejson) {
|
|
7
|
+
return validateStyleMin(stylejson).length === 0;
|
|
8
|
+
}
|
|
9
|
+
function isValidXyz(x, y, z) {
|
|
10
|
+
if (x < 0 || y < 0 || z < 0)
|
|
11
|
+
return false;
|
|
12
|
+
if (x >= 2 ** z || y >= 2 ** z)
|
|
13
|
+
return false;
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
function isSupportedFormat(ext) {
|
|
17
|
+
return ['png', 'jpeg', 'jpg', 'webp'].includes(ext);
|
|
18
|
+
}
|
|
19
|
+
function initServer(options) {
|
|
20
|
+
const tiles = new Hono()
|
|
21
|
+
.get('/:z/:x/:y_ext', async (c) => {
|
|
22
|
+
const url = c.req.query('url');
|
|
23
|
+
if (url === undefined)
|
|
24
|
+
return c.body('url is required', 400);
|
|
25
|
+
// path params
|
|
26
|
+
const z = Number(c.req.param('z'));
|
|
27
|
+
const x = Number(c.req.param('x'));
|
|
28
|
+
let [_y, ext] = c.req.param('y_ext').split('.');
|
|
29
|
+
const y = Number(_y);
|
|
30
|
+
if (!isValidXyz(x, y, z))
|
|
31
|
+
return c.body('invalid xyz', 400);
|
|
32
|
+
if (!isSupportedFormat(ext))
|
|
33
|
+
return c.body('invalid format', 400);
|
|
34
|
+
// query params
|
|
35
|
+
const tileSize = Number(c.req.query('tileSize') ?? 512);
|
|
36
|
+
const quality = Number(c.req.query('quality') ?? 100);
|
|
37
|
+
const margin = Number(c.req.query('margin') ?? 0);
|
|
38
|
+
let buf;
|
|
39
|
+
try {
|
|
40
|
+
buf = await getRenderedTileBuffer({
|
|
41
|
+
stylejson: url,
|
|
42
|
+
z,
|
|
43
|
+
x,
|
|
44
|
+
y,
|
|
45
|
+
tileSize,
|
|
46
|
+
cache: options.cache,
|
|
47
|
+
margin,
|
|
48
|
+
ext,
|
|
49
|
+
quality,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
console.error(`render error: ${e}`);
|
|
54
|
+
return c.body('failed to render tile', 400);
|
|
55
|
+
}
|
|
56
|
+
c.header('Content-Type', `image/${ext}`);
|
|
57
|
+
return c.body(buf);
|
|
58
|
+
})
|
|
59
|
+
.post('/:z/:x/:y_ext', async (c) => {
|
|
60
|
+
// body
|
|
61
|
+
const { style } = await c.req.json();
|
|
62
|
+
if (!isValidStylejson(style))
|
|
63
|
+
return c.body('invalid stylejson', 400);
|
|
64
|
+
// path params
|
|
65
|
+
const z = Number(c.req.param('z'));
|
|
66
|
+
const x = Number(c.req.param('x'));
|
|
67
|
+
let [_y, ext] = c.req.param('y_ext').split('.');
|
|
68
|
+
const y = Number(_y);
|
|
69
|
+
if (!isValidXyz(x, y, z))
|
|
70
|
+
return c.body('invalid xyz', 400);
|
|
71
|
+
if (!isSupportedFormat(ext))
|
|
72
|
+
return c.body('invalid format', 400);
|
|
73
|
+
// query params
|
|
74
|
+
const tileSize = Number(c.req.query('tileSize') ?? 512);
|
|
75
|
+
const quality = Number(c.req.query('quality') ?? 100);
|
|
76
|
+
const margin = Number(c.req.query('margin') ?? 0);
|
|
77
|
+
let buf;
|
|
78
|
+
try {
|
|
79
|
+
buf = await getRenderedTileBuffer({
|
|
80
|
+
stylejson: style,
|
|
81
|
+
z,
|
|
82
|
+
x,
|
|
83
|
+
y,
|
|
84
|
+
tileSize,
|
|
85
|
+
cache: options.cache,
|
|
86
|
+
margin,
|
|
87
|
+
ext,
|
|
88
|
+
quality,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
console.error(`render error: ${e}`);
|
|
93
|
+
return c.body('failed to render tile', 400);
|
|
94
|
+
}
|
|
95
|
+
c.header('Content-Type', `image/${ext}`);
|
|
96
|
+
return c.body(buf);
|
|
97
|
+
});
|
|
98
|
+
const clip = new Hono()
|
|
99
|
+
.get('/:filename_ext', async (c) => {
|
|
100
|
+
// path params
|
|
101
|
+
const [filename, ext] = c.req.param('filename_ext').split('.');
|
|
102
|
+
if (filename !== 'clip')
|
|
103
|
+
return c.body('not found', 404);
|
|
104
|
+
if (!isSupportedFormat(ext))
|
|
105
|
+
return c.body('invalid format', 400);
|
|
106
|
+
// query params
|
|
107
|
+
const bbox = c.req.query('bbox'); // ?bbox=minx,miny,maxx,maxy
|
|
108
|
+
if (bbox === undefined)
|
|
109
|
+
return c.body('bbox is required', 400);
|
|
110
|
+
const [minx, miny, maxx, maxy] = bbox.split(',').map(Number);
|
|
111
|
+
if (minx >= maxx || miny >= maxy)
|
|
112
|
+
return c.body('invalid bbox', 400);
|
|
113
|
+
const url = c.req.query('url');
|
|
114
|
+
if (url === undefined)
|
|
115
|
+
return c.body('url is required', 400);
|
|
116
|
+
const quality = Number(c.req.query('quality') ?? 100);
|
|
117
|
+
const size = Number(c.req.query('size') ?? 1024);
|
|
118
|
+
try {
|
|
119
|
+
const buf = await getRenderedBboxBuffer({
|
|
120
|
+
stylejson: url,
|
|
121
|
+
bbox: [minx, miny, maxx, maxy],
|
|
122
|
+
size,
|
|
123
|
+
cache: options.cache,
|
|
124
|
+
ext,
|
|
125
|
+
quality,
|
|
126
|
+
});
|
|
127
|
+
c.header('Content-Type', `image/${ext}`);
|
|
128
|
+
return c.body(buf);
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
console.error(`render error: ${e}`);
|
|
132
|
+
return c.body('failed to render bbox', 400);
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
.post('/:filename_ext', async (c) => {
|
|
136
|
+
// body
|
|
137
|
+
const { style } = await c.req.json();
|
|
138
|
+
if (!isValidStylejson(style))
|
|
139
|
+
return c.body('invalid stylejson', 400);
|
|
140
|
+
// path params
|
|
141
|
+
const [filename, ext] = c.req.param('filename_ext').split('.');
|
|
142
|
+
if (filename !== 'clip')
|
|
143
|
+
return c.body('not found', 404);
|
|
144
|
+
if (!isSupportedFormat(ext))
|
|
145
|
+
return c.body('invalid format', 400);
|
|
146
|
+
// query params
|
|
147
|
+
const bbox = c.req.query('bbox'); // ?bbox=minx,miny,maxx,maxy
|
|
148
|
+
if (bbox === undefined)
|
|
149
|
+
return c.body('bbox is required', 400);
|
|
150
|
+
const [minx, miny, maxx, maxy] = bbox.split(',').map(Number);
|
|
151
|
+
if (minx >= maxx || miny >= maxy)
|
|
152
|
+
return c.body('invalid bbox', 400);
|
|
153
|
+
const quality = Number(c.req.query('quality') ?? 100);
|
|
154
|
+
const size = Number(c.req.query('size') ?? 1024);
|
|
155
|
+
try {
|
|
156
|
+
const buf = await getRenderedBboxBuffer({
|
|
157
|
+
stylejson: style,
|
|
158
|
+
bbox: [minx, miny, maxx, maxy],
|
|
159
|
+
size,
|
|
160
|
+
cache: options.cache,
|
|
161
|
+
ext,
|
|
162
|
+
quality,
|
|
163
|
+
});
|
|
164
|
+
c.header('Content-Type', `image/${ext}`);
|
|
165
|
+
return c.body(buf);
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
console.error(`render error: ${e}`);
|
|
169
|
+
return c.body('failed to render bbox', 400);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
const hono = new Hono();
|
|
173
|
+
if (options.debug) {
|
|
174
|
+
hono.get('/debug', getDebugPage);
|
|
175
|
+
hono.get('/editor', getEditorgPage);
|
|
176
|
+
}
|
|
177
|
+
hono.get('/health', (c) => c.text('OK'));
|
|
178
|
+
hono.route('/tiles', tiles);
|
|
179
|
+
hono.route('/', clip);
|
|
180
|
+
return {
|
|
181
|
+
app: hono,
|
|
182
|
+
tiles,
|
|
183
|
+
clip,
|
|
184
|
+
start: () => serve({ port: options.port, fetch: hono.fetch }),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
export { initServer };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
function getFilesystemSource(uri) {
|
|
3
|
+
return new Promise((resolve, _) => {
|
|
4
|
+
fs.readFile(uri.replace('file://', ''), (err, data) => {
|
|
5
|
+
if (err) {
|
|
6
|
+
console.error(`[ERROR]: ${err}`);
|
|
7
|
+
resolve(null);
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
resolve(data);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export { getFilesystemSource };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
// for using native fetch in TypeScript
|
|
3
|
+
import { noneCache } from '../cache/index.js';
|
|
4
|
+
async function getHttpSource(uri, cache = noneCache()) {
|
|
5
|
+
// use cache only for http(s) sources
|
|
6
|
+
const val = await cache.get(uri);
|
|
7
|
+
if (val !== undefined)
|
|
8
|
+
return val; // hit
|
|
9
|
+
// miss
|
|
10
|
+
try {
|
|
11
|
+
const res = await fetch(uri);
|
|
12
|
+
if (!res.ok) {
|
|
13
|
+
console.log(`failed to fetch ${uri}`);
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
17
|
+
cache.set(uri, buf);
|
|
18
|
+
return buf;
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
console.error(`[ERROR] ${e}`);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export { getHttpSource };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
import { Cache } from '../cache/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* retrieve sources from the uri
|
|
5
|
+
* @param uri
|
|
6
|
+
* @param cache {Cache} - Cache Strategy. Affect only for http(s) sources.
|
|
7
|
+
* @returns
|
|
8
|
+
*/
|
|
9
|
+
declare function getSource(uri: string, cache?: Cache): Promise<Buffer | null>;
|
|
10
|
+
export { getSource };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { getFilesystemSource } from './fs.js';
|
|
2
|
+
import { getHttpSource } from './http.js';
|
|
3
|
+
import { getPmtilesSoruce } from './pmtiles.js';
|
|
4
|
+
import { getMbtilesSource } from './mbtiles.js';
|
|
5
|
+
import { getS3Source } from './s3.js';
|
|
6
|
+
import { noneCache } from '../cache/index.js';
|
|
7
|
+
/**
|
|
8
|
+
* retrieve sources from the uri
|
|
9
|
+
* @param uri
|
|
10
|
+
* @param cache {Cache} - Cache Strategy. Affect only for http(s) sources.
|
|
11
|
+
* @returns
|
|
12
|
+
*/
|
|
13
|
+
async function getSource(uri, cache = noneCache()) {
|
|
14
|
+
let data = null;
|
|
15
|
+
if (uri.startsWith('http://') || uri.startsWith('https://'))
|
|
16
|
+
data = await getHttpSource(uri, cache);
|
|
17
|
+
else if (uri.startsWith('file://'))
|
|
18
|
+
data = await getFilesystemSource(uri);
|
|
19
|
+
else if (uri.startsWith('s3://'))
|
|
20
|
+
data = await getS3Source(uri);
|
|
21
|
+
else if (uri.startsWith('mbtiles://'))
|
|
22
|
+
data = await getMbtilesSource(uri);
|
|
23
|
+
else if (uri.startsWith('pmtiles://'))
|
|
24
|
+
data = await getPmtilesSoruce(uri, cache);
|
|
25
|
+
return data;
|
|
26
|
+
}
|
|
27
|
+
export { getSource };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { unzip } from 'zlib';
|
|
3
|
+
import { LRUCache } from 'lru-cache';
|
|
4
|
+
const mbtilesCache = new LRUCache({
|
|
5
|
+
max: 20,
|
|
6
|
+
});
|
|
7
|
+
/**
|
|
8
|
+
* uri = mbtiles://path/to/file.mbtiles/{z}/{x}/{y}
|
|
9
|
+
*/
|
|
10
|
+
async function getMbtilesSource(uri) {
|
|
11
|
+
const mbtilesFilepath = uri
|
|
12
|
+
.replace('mbtiles://', '')
|
|
13
|
+
.replace(/\/\d+\/\d+\/\d+$/, '');
|
|
14
|
+
let statement = mbtilesCache.get(mbtilesFilepath);
|
|
15
|
+
if (statement === undefined) {
|
|
16
|
+
const db = new Database(mbtilesFilepath, { readonly: true });
|
|
17
|
+
statement = db.prepare('SELECT tile_data FROM tiles WHERE zoom_level = ? AND tile_column = ? AND tile_row = ?');
|
|
18
|
+
mbtilesCache.set(mbtilesFilepath, statement);
|
|
19
|
+
}
|
|
20
|
+
const [z, x, y] = uri
|
|
21
|
+
.replace(`mbtiles://${mbtilesFilepath}/`, '')
|
|
22
|
+
.split('/');
|
|
23
|
+
const ty = Math.pow(2, Number(z)) - 1 - Number(y);
|
|
24
|
+
const row = statement.get(z, x, ty);
|
|
25
|
+
if (!row)
|
|
26
|
+
return null;
|
|
27
|
+
const unzipped = await new Promise((resolve, _) => {
|
|
28
|
+
unzip(row.tile_data, (err, buffer) => {
|
|
29
|
+
if (err) {
|
|
30
|
+
console.error(`[ERROR]: ${err}`);
|
|
31
|
+
resolve(null);
|
|
32
|
+
}
|
|
33
|
+
else
|
|
34
|
+
resolve(buffer);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
return unzipped;
|
|
38
|
+
}
|
|
39
|
+
export { getMbtilesSource };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
import { type Cache } from '../cache/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* uri = pmtiles://path/to/file.pmtiles/{z}/{x}/{y}
|
|
5
|
+
* uri = pmtiles://http://url/to/file.pmtiles/{z}/{x}/{y}
|
|
6
|
+
* uri = pmtiles://s3://bucket/key/to/file.pmtiles/{z}/{x}/{y}
|
|
7
|
+
*/
|
|
8
|
+
declare function getPmtilesSoruce(uri: string, cache?: Cache): Promise<Buffer | null>;
|
|
9
|
+
export { getPmtilesSoruce };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import { PMTiles } from 'pmtiles';
|
|
3
|
+
import { GetObjectCommand } from '@aws-sdk/client-s3';
|
|
4
|
+
import { LRUCache } from 'lru-cache';
|
|
5
|
+
import { getS3Client } from '../s3.js';
|
|
6
|
+
import { noneCache } from '../cache/index.js';
|
|
7
|
+
const pmtilesCache = new LRUCache({
|
|
8
|
+
max: 50,
|
|
9
|
+
});
|
|
10
|
+
class FilesystemSource {
|
|
11
|
+
filepath;
|
|
12
|
+
fileHandle;
|
|
13
|
+
constructor(filepath) {
|
|
14
|
+
this.filepath = filepath;
|
|
15
|
+
this.fileHandle = fs.promises.open(filepath, 'r');
|
|
16
|
+
}
|
|
17
|
+
getKey() {
|
|
18
|
+
return this.filepath;
|
|
19
|
+
}
|
|
20
|
+
async getBytes(offset, length) {
|
|
21
|
+
const buf = Buffer.alloc(length);
|
|
22
|
+
await (await this.fileHandle).read(buf, 0, length, offset);
|
|
23
|
+
return { data: buf.buffer };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
class S3Source {
|
|
27
|
+
bucket;
|
|
28
|
+
key;
|
|
29
|
+
s3Client;
|
|
30
|
+
constructor(bucket, key) {
|
|
31
|
+
this.bucket = bucket;
|
|
32
|
+
this.key = key;
|
|
33
|
+
this.s3Client = getS3Client({
|
|
34
|
+
region: process.env.CHIITILER_S3_REGION ?? 'us-east1',
|
|
35
|
+
endpoint: process.env.CHIITILER_S3_ENDPOINT ?? null,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
getKey() {
|
|
39
|
+
return `s3://${this.bucket}/${this.key}`;
|
|
40
|
+
}
|
|
41
|
+
async getBytes(offset, length) {
|
|
42
|
+
const cmd = new GetObjectCommand({
|
|
43
|
+
Bucket: this.bucket,
|
|
44
|
+
Key: this.key,
|
|
45
|
+
Range: `bytes=${offset}-${offset + length - 1}`,
|
|
46
|
+
});
|
|
47
|
+
try {
|
|
48
|
+
const obj = await this.s3Client.send(cmd);
|
|
49
|
+
if (obj.Body === undefined)
|
|
50
|
+
return { data: Buffer.alloc(0).buffer };
|
|
51
|
+
const buf = Buffer.from(await obj.Body.transformToByteArray());
|
|
52
|
+
return { data: buf.buffer };
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
if (e.name !== 'NoSuchKey')
|
|
56
|
+
console.log(e);
|
|
57
|
+
return { data: Buffer.alloc(0).buffer };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* uri = pmtiles://path/to/file.pmtiles/{z}/{x}/{y}
|
|
63
|
+
* uri = pmtiles://http://url/to/file.pmtiles/{z}/{x}/{y}
|
|
64
|
+
* uri = pmtiles://s3://bucket/key/to/file.pmtiles/{z}/{x}/{y}
|
|
65
|
+
*/
|
|
66
|
+
async function getPmtilesSoruce(uri, cache = noneCache()) {
|
|
67
|
+
const pmtilesUri = uri
|
|
68
|
+
.replace('pmtiles://', '')
|
|
69
|
+
.replace(/\/\d+\/\d+\/\d+$/, '');
|
|
70
|
+
let pmtiles = pmtilesCache.get(pmtilesUri);
|
|
71
|
+
const isHttpSource = pmtilesUri.startsWith('http://') || pmtilesUri.startsWith('https://');
|
|
72
|
+
if (isHttpSource) {
|
|
73
|
+
const val = await cache.get(uri);
|
|
74
|
+
if (val !== undefined)
|
|
75
|
+
return val; // hit
|
|
76
|
+
if (pmtiles === undefined) {
|
|
77
|
+
pmtiles = new PMTiles(pmtilesUri);
|
|
78
|
+
pmtilesCache.set(pmtilesUri, pmtiles);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else if (pmtilesUri.startsWith('s3://')) {
|
|
82
|
+
if (pmtiles === undefined) {
|
|
83
|
+
const bucket = pmtilesUri.replace('s3://', '').split('/')[0];
|
|
84
|
+
const key = pmtilesUri.replace(`s3://${bucket}/`, '');
|
|
85
|
+
const s3Source = new S3Source(bucket, key);
|
|
86
|
+
pmtiles = new PMTiles(s3Source);
|
|
87
|
+
pmtilesCache.set(pmtilesUri, pmtiles);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
if (pmtiles === undefined) {
|
|
92
|
+
const fileSource = new FilesystemSource(pmtilesUri);
|
|
93
|
+
pmtiles = new PMTiles(fileSource);
|
|
94
|
+
pmtilesCache.set(pmtilesUri, pmtiles);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const [z, x, y] = uri.replace(`pmtiles://${pmtilesUri}/`, '').split('/');
|
|
98
|
+
const tile = await pmtiles.getZxy(Number(z), Number(x), Number(y));
|
|
99
|
+
if (!tile)
|
|
100
|
+
return null;
|
|
101
|
+
const buf = Buffer.from(tile.data);
|
|
102
|
+
if (isHttpSource)
|
|
103
|
+
cache.set(uri, buf);
|
|
104
|
+
return buf;
|
|
105
|
+
}
|
|
106
|
+
export { getPmtilesSoruce };
|