Initial commit: saint-malo navigation quiz
Build and Push Docker Image / build (push) Failing after 5m2s

This commit is contained in:
2026-06-11 10:04:44 +02:00
commit c3cc578d1c
10 changed files with 1861 additions and 0 deletions
+127
View File
@@ -0,0 +1,127 @@
const express = require('express');
const https = require('https');
const path = require('path');
const fs = require('fs');
const app = express();
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
const POINTS_FILE = path.join(__dirname, 'data', 'points.json');
const TILES_DIR = path.join(__dirname, 'tiles');
const SHOM_BASE = 'https://services.data.shom.fr/clevisu/wmts?layer=RASTER_MARINE_3857_WMTS&style=normal&tilematrixset=3857&Service=WMTS&Request=GetTile&Version=1.0.0&Format=image%2Fpng';
const SHOM_HEADERS = {
'Referer': 'https://data.shom.fr',
'Origin': 'https://data.shom.fr',
'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/114.0',
'Accept': 'image/png'
};
let db = null;
let mbtilesMeta = {};
let tileStmt = null;
async function initMbtiles() {
try {
const files = fs.readdirSync(TILES_DIR).filter(f => f.endsWith('.mbtiles'));
if (files.length === 0) return;
const initSqlJs = require('sql.js');
const SQL = await initSqlJs();
const buf = fs.readFileSync(path.join(TILES_DIR, files[0]));
db = new SQL.Database(buf);
db.each('SELECT name, value FROM metadata', [], (row) => {
mbtilesMeta[row.name] = row.value;
});
tileStmt = db.prepare(
'SELECT tile_data FROM tiles WHERE zoom_level=? AND tile_column=? AND tile_row=?'
);
console.log(`✓ MBTiles : ${files[0]} (zoom ${mbtilesMeta.minzoom}${mbtilesMeta.maxzoom})`);
} catch (e) {
console.log('Pas de MBTiles local, utilisation du flux SHOM en ligne.');
}
}
function proxyShom(z, x, y, res) {
const url = `${SHOM_BASE}&TileMatrix=${z}&TileCol=${x}&TileRow=${y}`;
https.get(url, { headers: SHOM_HEADERS }, (upstream) => {
if (upstream.statusCode !== 200) return res.status(upstream.statusCode).end();
res.set('Content-Type', 'image/png');
res.set('Cache-Control', 'public, max-age=86400');
upstream.pipe(res);
}).on('error', () => res.status(502).end());
}
initMbtiles().then(startServer);
app.get('/api/status', (req, res) => {
res.json({
hasMbtiles: !!db,
name: mbtilesMeta.name || null,
minZoom: parseInt(mbtilesMeta.minzoom) || 6,
maxZoom: parseInt(mbtilesMeta.maxzoom) || 17
});
});
app.get('/tiles/:z/:x/:y', (req, res) => {
const z = parseInt(req.params.z);
const x = parseInt(req.params.x);
const y = parseInt(req.params.y);
if (tileStmt) {
const tmsY = (1 << z) - 1 - y;
try {
tileStmt.bind([z, x, tmsY]);
if (!tileStmt.step()) { tileStmt.reset(); return proxyShom(z, x, y, res); }
const row = tileStmt.getAsObject();
tileStmt.reset();
const fmt = mbtilesMeta.format || 'png';
res.set('Content-Type', fmt === 'jpg' || fmt === 'jpeg' ? 'image/jpeg' : 'image/png');
res.set('Cache-Control', 'public, max-age=86400');
res.send(Buffer.from(row.tile_data));
} catch (e) {
tileStmt.reset();
proxyShom(z, x, y, res);
}
} else {
proxyShom(z, x, y, res);
}
});
function loadPoints() {
if (!fs.existsSync(POINTS_FILE)) return [];
try { return JSON.parse(fs.readFileSync(POINTS_FILE, 'utf8')); }
catch { return []; }
}
function savePoints(pts) {
fs.writeFileSync(POINTS_FILE, JSON.stringify(pts, null, 2));
}
app.get('/api/points', (req, res) => res.json(loadPoints()));
app.post('/api/points', (req, res) => {
const pts = loadPoints();
const pt = { id: `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`, ...req.body };
pts.push(pt);
savePoints(pts);
res.json(pt);
});
app.put('/api/points/:id', (req, res) => {
const pts = loadPoints();
const i = pts.findIndex(p => p.id === req.params.id);
if (i === -1) return res.status(404).json({ error: 'Not found' });
pts[i] = { ...pts[i], ...req.body };
savePoints(pts);
res.json(pts[i]);
});
app.delete('/api/points/:id', (req, res) => {
savePoints(loadPoints().filter(p => p.id !== req.params.id));
res.json({ ok: true });
});
function startServer() {
app.listen(4242, () => console.log('\n⚓ Saint-Malo Quiz → http://localhost:4242\n'));
}