-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
104 lines (84 loc) · 3.15 KB
/
server.js
File metadata and controls
104 lines (84 loc) · 3.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
const fastify = require('fastify')({ logger: { level: 'error' } });
const Database = require('better-sqlite3');
const path = require('path');
const fs = require('fs');
const PORT = 3000;
const DATA_DIR = path.join(__dirname, 'data');
// Add CORS
fastify.register(require('@fastify/cors'), {
origin: '*',
methods: ['GET']
});
// Auto-detect mbtiles file
let mbtilesFile = 'local.mbtiles';
try {
if (fs.existsSync(DATA_DIR)) {
const files = fs.readdirSync(DATA_DIR).filter(file => file.endsWith('.mbtiles'));
if (files.length > 0) mbtilesFile = files[0];
}
} catch (e) {
console.error('Failed to scan data directory:', e.message);
}
const MBTILES_PATH = path.join(DATA_DIR, mbtilesFile);
if (!fs.existsSync(MBTILES_PATH)) {
console.error(`Error: No .mbtiles file found at ${MBTILES_PATH}`);
process.exit(1);
}
console.log(`Serving tiles from: ${mbtilesFile}`);
const db = new Database(MBTILES_PATH, { readonly: true });
const getTileStmt = db.prepare('SELECT tile_data FROM tiles WHERE zoom_level = ? AND tile_column = ? AND tile_row = ?');
// Determine content type
let contentType = 'application/octet-stream';
try {
const formatRow = db.prepare('SELECT value FROM metadata WHERE name = ?').get('format');
if (formatRow) {
switch (formatRow.value) {
case 'pbf': contentType = 'application/x-protobuf'; break;
case 'png': contentType = 'image/png'; break;
case 'jpg': case 'jpeg': contentType = 'image/jpeg'; break;
case 'webp': contentType = 'image/webp'; break;
}
}
} catch (err) {
console.warn('Metadata read failed, defaulting to application/octet-stream');
}
fastify.get('/', async (req, reply) => {
return { status: 'running', service: 'local-map-tiler', tiles_endpoint: '/tiles/{z}/{x}/{y}' };
});
fastify.get('/tiles/:z/:x/:y', async (req, reply) => {
const { z, x, y } = req.params;
// TMS Y-coordinate flip: (2^z - 1) - y
const zInt = parseInt(z);
const xInt = parseInt(x);
const yInt = parseInt(y); // Fastify handles standard int parsing, but we can be safe
// Handle extension if present (e.g. 123.png)
const yVal = isNaN(yInt) ? parseInt(y.split('.')[0]) : yInt;
const tileRow = (Math.pow(2, zInt) - 1) - yVal;
try {
const row = getTileStmt.get(zInt, xInt, tileRow);
if (!row || !row.tile_data) {
return reply.code(404).send('Not Found');
}
const buffer = row.tile_data;
reply.header('Content-Type', contentType);
reply.header('Cache-Control', 'public, max-age=31536000');
// GZIP detection (0x1f 0x8b)
if (buffer.length >= 2 && buffer[0] === 0x1f && buffer[1] === 0x8b) {
reply.header('Content-Encoding', 'gzip');
}
return reply.send(buffer);
} catch (err) {
req.log.error(err);
return reply.code(500).send('Internal Server Error');
}
});
const start = async () => {
try {
await fastify.listen({ port: PORT, host: '0.0.0.0' });
console.log(`Server running at http://localhost:${PORT}`);
} catch (err) {
console.error(err);
process.exit(1);
}
};
start();