From 78d40c2b8534b20ed942aa57a27ea2fe7acd4055 Mon Sep 17 00:00:00 2001 From: Occumed79 Date: Thu, 30 Jul 2026 07:44:19 -0700 Subject: [PATCH 01/10] Add flat overview server --- server-flat.mjs | 308 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 server-flat.mjs diff --git a/server-flat.mjs b/server-flat.mjs new file mode 100644 index 0000000..19f46a4 --- /dev/null +++ b/server-flat.mjs @@ -0,0 +1,308 @@ +import { createHash, randomUUID } from 'node:crypto'; +import fs from 'node:fs/promises'; +import http from 'node:http'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { PMTiles } from 'pmtiles'; + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url))); +const publicRoot = path.join(repositoryRoot, 'dist'); +const archivePath = path.join(publicRoot, 'virtual-assets', 'occumed-world-overview.pmtiles'); +const port = Number(process.env.PORT || 4173); +const host = process.env.HOST?.trim() || '0.0.0.0'; +const configuredMaxZoom = Number(process.env.OCCUMED_FLAT_MAX_ZOOM || 5); + +const contentTypes = { + '.css': 'text/css; charset=utf-8', + '.html': 'text/html; charset=utf-8', + '.js': 'text/javascript; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.map': 'application/json; charset=utf-8', + '.pbf': 'application/x-protobuf', + '.png': 'image/png', + '.svg': 'image/svg+xml; charset=utf-8', + '.webp': 'image/webp' +}; + +const commonHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Cache-Control, If-None-Match', + 'Access-Control-Expose-Headers': 'Content-Length, ETag, X-Occumed-Request-Id, X-Occumed-Tileset', + 'Cross-Origin-Resource-Policy': 'cross-origin', + 'Permissions-Policy': 'camera=(), microphone=(), geolocation=(), payment=(), usb=()', + 'Referrer-Policy': 'strict-origin-when-cross-origin', + 'X-Content-Type-Options': 'nosniff' +}; + +class LocalPmtilesSource { + constructor(filename) { + this.filename = filename; + this.handlePromise = fs.open(filename, 'r'); + } + + getKey() { + return this.filename; + } + + async getBytes(offset, length) { + const handle = await this.handlePromise; + const requestedLength = Number(length); + const requestedOffset = Number(offset); + if (!Number.isSafeInteger(requestedLength) || requestedLength < 0) { + throw new RangeError('Invalid PMTiles byte length.'); + } + if (!Number.isSafeInteger(requestedOffset) || requestedOffset < 0) { + throw new RangeError('Invalid PMTiles byte offset.'); + } + + const buffer = Buffer.allocUnsafe(requestedLength); + const { bytesRead } = await handle.read(buffer, 0, requestedLength, requestedOffset); + if (bytesRead !== requestedLength) { + throw new Error(`PMTiles short read: requested ${requestedLength}, received ${bytesRead}.`); + } + return { data: buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) }; + } + + async close() { + const handle = await this.handlePromise; + await handle.close(); + } +} + +function requestId(value) { + const candidate = String(value || '').trim(); + return /^[A-Za-z0-9._:-]{1,128}$/.test(candidate) ? candidate : randomUUID(); +} + +function safeOrigin(request) { + const configured = process.env.PUBLIC_ORIGIN?.trim().replace(/\/$/, ''); + if (configured) { + try { + const parsed = new URL(configured); + if (['http:', 'https:'].includes(parsed.protocol) && !parsed.username && !parsed.password) { + return parsed.origin; + } + } catch { + // Fall through to request-derived origin. + } + } + + const forwardedProtocol = String(request.headers['x-forwarded-proto'] || '').split(',')[0].trim(); + const protocol = /^(?:http|https)$/.test(forwardedProtocol) ? forwardedProtocol : 'http'; + const forwardedHost = String(request.headers['x-forwarded-host'] || '').split(',')[0].trim(); + const requestHost = forwardedHost || String(request.headers.host || `localhost:${port}`).trim(); + return `${protocol}://${requestHost}`; +} + +function write(response, status, body, contentType, method = 'GET', cacheControl = 'no-store', extra = {}) { + const payload = Buffer.isBuffer(body) ? body : Buffer.from(String(body ?? '')); + response.writeHead(status, { + ...commonHeaders, + 'X-Occumed-Request-Id': response.occumedRequestId, + 'Cache-Control': cacheControl, + 'Content-Length': payload.byteLength, + 'Content-Type': contentType, + ...extra + }); + if (method === 'HEAD' || status === 304) response.end(); + else response.end(payload); +} + +function writeJson(response, status, value, method = 'GET', extra = {}) { + write(response, status, `${JSON.stringify(value)}\n`, contentTypes['.json'], method, 'no-store', extra); +} + +function normalizeCoordinates(zText, xText, yText, maxZoom) { + const z = Number(zText); + const x = Number(xText); + const y = Number(yText); + if (![z, x, y].every(Number.isSafeInteger)) return null; + if (z < 0 || z > maxZoom) return null; + const width = 2 ** z; + if (x < 0 || x >= width || y < 0 || y >= width) return null; + return { z, x, y }; +} + +function shouldServeSpa(request, pathname) { + return pathname === '/' || (!path.extname(pathname) && String(request.headers.accept || '').includes('text/html')); +} + +async function serveStatic(request, response, pathname) { + const requested = pathname === '/' ? '/index.html' : pathname; + let decoded; + try { + decoded = decodeURIComponent(requested); + } catch { + write(response, 400, 'Invalid URL encoding', 'text/plain; charset=utf-8', request.method); + return; + } + + if (decoded.includes('\0')) { + write(response, 400, 'Invalid path', 'text/plain; charset=utf-8', request.method); + return; + } + + const absolute = path.resolve(publicRoot, `.${decoded}`); + if (!absolute.startsWith(`${publicRoot}${path.sep}`) && absolute !== path.join(publicRoot, 'index.html')) { + write(response, 403, 'Forbidden', 'text/plain; charset=utf-8', request.method); + return; + } + + try { + const stat = await fs.stat(absolute); + if (!stat.isFile()) throw new Error('Not a file'); + const extension = path.extname(absolute).toLowerCase(); + const body = await fs.readFile(absolute); + const immutable = decoded.startsWith('/assets/') || decoded.startsWith('/sprites/'); + write( + response, + 200, + body, + contentTypes[extension] || 'application/octet-stream', + request.method, + immutable ? 'public, max-age=31536000, immutable' : 'no-store, max-age=0' + ); + } catch { + if (!shouldServeSpa(request, decoded)) { + write(response, 404, 'Not found', 'text/plain; charset=utf-8', request.method); + return; + } + const index = await fs.readFile(path.join(publicRoot, 'index.html')); + write(response, 200, index, contentTypes['.html'], request.method, 'no-store, max-age=0'); + } +} + +const source = new LocalPmtilesSource(archivePath); +const archive = new PMTiles(source); +const header = await archive.getHeader(); +const maxZoom = Math.min( + Number.isSafeInteger(configuredMaxZoom) ? configuredMaxZoom : 5, + Number(header.maxZoom) +); + +if (!Number.isSafeInteger(maxZoom) || maxZoom < 0) { + throw new Error('The flat overview PMTiles archive has an invalid maximum zoom.'); +} + +let shuttingDown = false; +let activeRequests = 0; + +async function handle(request, response) { + const method = request.method || 'GET'; + if (method === 'OPTIONS') { + response.writeHead(204, { ...commonHeaders, 'Cache-Control': 'no-store' }); + response.end(); + return; + } + if (!['GET', 'HEAD'].includes(method)) { + write(response, 405, 'Method not allowed', 'text/plain; charset=utf-8', method, 'no-store', { Allow: 'GET, HEAD, OPTIONS' }); + return; + } + if (shuttingDown) { + write(response, 503, 'Server is shutting down', 'text/plain; charset=utf-8', method, 'no-store', { 'Retry-After': '5' }); + return; + } + + const url = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`); + if (url.pathname === '/health' || url.pathname === '/healthz') { + write(response, 200, 'ok', 'text/plain; charset=utf-8', method); + return; + } + if (url.pathname === '/readyz') { + writeJson(response, 200, { + ready: true, + mode: 'flat-overview-only', + projection: 'mercator', + minZoom: Number(header.minZoom), + maxZoom, + activeRequests, + shuttingDown + }, method); + return; + } + if (url.pathname === '/style/occumed-open.json') { + const styleText = await fs.readFile(path.join(publicRoot, 'style', 'occumed-open.json'), 'utf8'); + const resolved = styleText.replaceAll('__OCCUMED_PUBLIC_ORIGIN__', safeOrigin(request)); + write(response, 200, resolved, contentTypes['.json'], method, 'no-store, max-age=0'); + return; + } + + const tileMatch = /^\/tiles\/(\d+)\/(\d+)\/(\d+)\.pbf$/.exec(url.pathname); + if (tileMatch) { + const coordinates = normalizeCoordinates(tileMatch[1], tileMatch[2], tileMatch[3], maxZoom); + if (!coordinates) { + write(response, 404, 'Tile not found', 'text/plain; charset=utf-8', method); + return; + } + + activeRequests += 1; + try { + const result = await archive.getZxy(coordinates.z, coordinates.x, coordinates.y); + if (!result?.data) { + write(response, 204, Buffer.alloc(0), contentTypes['.pbf'], method, 'public, max-age=3600'); + return; + } + const tile = Buffer.from(result.data); + const etag = `"${createHash('sha256').update(tile).digest('base64url').slice(0, 24)}"`; + if (request.headers['if-none-match'] === etag) { + write(response, 304, Buffer.alloc(0), contentTypes['.pbf'], method, 'public, max-age=3600', { + ETag: etag, + 'X-Occumed-Tileset': 'flat-overview-only-v1' + }); + return; + } + write(response, 200, tile, contentTypes['.pbf'], method, 'public, max-age=3600, stale-if-error=86400', { + ETag: etag, + 'X-Occumed-Tileset': 'flat-overview-only-v1' + }); + } finally { + activeRequests -= 1; + } + return; + } + + await serveStatic(request, response, url.pathname); +} + +const server = http.createServer((request, response) => { + response.occumedRequestId = requestId(request.headers['x-request-id']); + void handle(request, response).catch((error) => { + console.error(JSON.stringify({ + level: 'error', + type: 'flat-request-failed', + requestId: response.occumedRequestId, + code: error?.code || error?.name || 'UNKNOWN', + message: error?.message || String(error) + })); + if (!response.headersSent) { + write(response, 500, 'Internal server error', 'text/plain; charset=utf-8', request.method); + } else { + response.destroy(); + } + }); +}); + +server.requestTimeout = 45_000; +server.headersTimeout = 20_000; +server.keepAliveTimeout = 5_000; +server.maxHeadersCount = 100; + +function shutdown(signal) { + if (shuttingDown) return; + shuttingDown = true; + console.log(`Flat overview server received ${signal}; draining connections.`); + server.close(() => { + void source.close().catch((error) => console.error('Unable to close PMTiles source:', error)); + }); + const timer = setTimeout(() => server.closeAllConnections?.(), 20_000); + timer.unref?.(); +} + +process.once('SIGTERM', () => shutdown('SIGTERM')); +process.once('SIGINT', () => shutdown('SIGINT')); + +server.listen(port, host, () => { + console.log(`Occu-Med flat overview server listening on ${host}:${port}.`); + console.log(`Serving one immutable Mercator overview source through zoom ${maxZoom}; globe, Neon, and runtime tile synthesis are disabled.`); +}); From 8e1c3761014d77ec5468180a642b9dd207acf87d Mon Sep 17 00:00:00 2001 From: Occumed79 Date: Thu, 30 Jul 2026 07:44:51 -0700 Subject: [PATCH 02/10] Add flat overview starter --- scripts/start-flat-overview.mjs | 115 ++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 scripts/start-flat-overview.mjs diff --git a/scripts/start-flat-overview.mjs b/scripts/start-flat-overview.mjs new file mode 100644 index 0000000..4e25eec --- /dev/null +++ b/scripts/start-flat-overview.mjs @@ -0,0 +1,115 @@ +#!/usr/bin/env node + +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { spawn } from 'node:child_process'; +import { Readable } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; +import { fileURLToPath } from 'node:url'; + +const repository = process.env.OCCUMED_WORLD_RELEASE_REPOSITORY?.trim() || 'Occumed79/Map'; +const tag = process.env.OCCUMED_WORLD_RELEASE_TAG?.trim() || 'occumed-world-v1'; +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const assetDir = path.join(root, 'dist', 'virtual-assets'); +const assetName = 'occumed-world-overview.pmtiles'; +const destination = path.join(assetDir, assetName); +const explicitUrl = process.env.OCCUMED_WORLD_OVERVIEW_SOURCE_URL?.trim(); +const maxAssetBytes = Number(process.env.OCCUMED_FLAT_ASSET_MAX_BYTES || 512 * 1024 * 1024); + +function releaseUrl() { + return `https://github.com/${repository}/releases/download/${encodeURIComponent(tag)}/${encodeURIComponent(assetName)}`; +} + +async function validArchive(filename) { + try { + const stat = await fs.stat(filename); + if (!stat.isFile() || stat.size < 127 || stat.size > maxAssetBytes) return false; + const handle = await fs.open(filename, 'r'); + try { + const magic = Buffer.alloc(7); + const { bytesRead } = await handle.read(magic, 0, magic.length, 0); + return bytesRead === 7 && magic.toString('utf8') === 'PMTiles'; + } finally { + await handle.close(); + } + } catch { + return false; + } +} + +async function localizeArchive() { + if (await validArchive(destination)) { + console.log(`Using localized immutable archive ${assetName}.`); + return; + } + + const sourceUrl = explicitUrl || releaseUrl(); + const parsed = new URL(sourceUrl); + if (!['http:', 'https:'].includes(parsed.protocol)) { + throw new Error('Flat overview source must use HTTP or HTTPS.'); + } + + await fs.mkdir(assetDir, { recursive: true }); + const temporary = `${destination}.${process.pid}.tmp`; + await fs.rm(temporary, { force: true }); + + const controller = new AbortController(); + const timeout = setTimeout( + () => controller.abort(new Error(`Download timed out for ${assetName}.`)), + 10 * 60_000 + ); + timeout.unref?.(); + + try { + const response = await fetch(parsed, { + redirect: 'follow', + signal: controller.signal, + headers: { 'User-Agent': 'Occu-Med-Map/flat-overview-localizer' } + }); + if (!response.ok || !response.body) { + throw new Error(`Flat overview archive returned HTTP ${response.status}.`); + } + + const declaredLength = Number(response.headers.get('content-length')); + if (Number.isFinite(declaredLength) && declaredLength > maxAssetBytes) { + throw new Error(`Flat overview archive exceeds ${maxAssetBytes} bytes.`); + } + + await pipeline( + Readable.fromWeb(response.body), + await fs.open(temporary, 'w').then((handle) => handle.createWriteStream()) + ); + + if (!(await validArchive(temporary))) { + throw new Error('Downloaded flat overview archive failed PMTiles validation.'); + } + + await fs.rename(temporary, destination); + console.log(`Localized immutable overview archive ${assetName}.`); + } finally { + clearTimeout(timeout); + await fs.rm(temporary, { force: true }).catch(() => {}); + } +} + +try { + await localizeArchive(); +} catch (error) { + console.error(`Unable to prepare flat overview archive: ${error.message}`); + process.exit(1); +} + +const child = spawn(process.execPath, ['server-flat.mjs'], { + cwd: root, + env: { ...process.env }, + stdio: 'inherit' +}); + +for (const signal of ['SIGINT', 'SIGTERM']) { + process.on(signal, () => child.kill(signal)); +} + +child.on('exit', (code, signal) => { + if (signal) process.kill(process.pid, signal); + else process.exit(code ?? 1); +}); From fa95d596afed24461bbb59e4faf5c9496ffee623 Mon Sep 17 00:00:00 2001 From: Occumed79 Date: Thu, 30 Jul 2026 07:45:57 -0700 Subject: [PATCH 03/10] Lock flat Mercator overview style --- scripts/apply-flat-overview-mode.mjs | 45 ++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 scripts/apply-flat-overview-mode.mjs diff --git a/scripts/apply-flat-overview-mode.mjs b/scripts/apply-flat-overview-mode.mjs new file mode 100644 index 0000000..a91d05e --- /dev/null +++ b/scripts/apply-flat-overview-mode.mjs @@ -0,0 +1,45 @@ +#!/usr/bin/env node + +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const stylePath = path.join(root, 'public', 'style', 'occumed-open.json'); +const style = JSON.parse(await fs.readFile(stylePath, 'utf8')); +const source = style.sources?.['occumed-open']; + +if (!source || source.type !== 'vector') { + throw new Error('Flat overview mode requires the existing occumed-open vector source.'); +} + +style.sources = { + 'occumed-open': { + ...source, + tiles: ['__OCCUMED_PUBLIC_ORIGIN__/tiles/{z}/{x}/{y}.pbf'], + minzoom: 0, + maxzoom: 5 + } +}; + +style.layers = (style.layers || []).filter((layer) => + (!layer.source || layer.source === 'occumed-open') && + !['sky', 'hillshade', 'model'].includes(layer.type) +); + +style.projection = { type: 'mercator' }; +delete style.terrain; +delete style.fog; + +style.metadata = { + ...(style.metadata || {}), + 'occumed:emergency-mode': 'immutable-flat-overview-only', + 'occumed:projection': 'mercator', + 'occumed:runtime-geometry': false, + 'occumed:neon-cache': false, + 'occumed:regional-routing': false, + 'occumed:atmosphere': false +}; + +await fs.writeFile(stylePath, `${JSON.stringify(style, null, 2)}\n`); +console.log(`Flat overview locked: Mercator, one vector source, ${style.layers.length} layers, maxzoom 5.`); From 44d992525e10df7a1c136803db45afc8ca0cd1ff Mon Sep 17 00:00:00 2001 From: Occumed79 Date: Thu, 30 Jul 2026 07:46:27 -0700 Subject: [PATCH 04/10] Validate flat overview contract --- scripts/check-flat-overview-mode.mjs | 65 ++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 scripts/check-flat-overview-mode.mjs diff --git a/scripts/check-flat-overview-mode.mjs b/scripts/check-flat-overview-mode.mjs new file mode 100644 index 0000000..089b602 --- /dev/null +++ b/scripts/check-flat-overview-mode.mjs @@ -0,0 +1,65 @@ +#!/usr/bin/env node + +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const failures = []; +const expect = (condition, message) => { + if (!condition) failures.push(message); +}; + +const [style, server, starter, packageJson, mainCss] = await Promise.all([ + fs.readFile(path.join(root, 'public', 'style', 'occumed-open.json'), 'utf8').then(JSON.parse), + fs.readFile(path.join(root, 'server-flat.mjs'), 'utf8'), + fs.readFile(path.join(root, 'scripts', 'start-flat-overview.mjs'), 'utf8'), + fs.readFile(path.join(root, 'package.json'), 'utf8').then(JSON.parse), + fs.readFile(path.join(root, 'src', 'flat-overview.css'), 'utf8') +]); + +const sourceEntries = Object.entries(style.sources || {}); +expect(sourceEntries.length === 1, `Flat style must contain exactly one source; found ${sourceEntries.length}.`); +expect(sourceEntries[0]?.[0] === 'occumed-open', 'Flat vector source ID changed.'); +expect(sourceEntries[0]?.[1]?.type === 'vector', 'Flat source is not vector data.'); +expect(sourceEntries[0]?.[1]?.maxzoom === 5, 'Flat source must stop at immutable overview zoom 5.'); +expect( + JSON.stringify(sourceEntries[0]?.[1]?.tiles) === JSON.stringify(['__OCCUMED_PUBLIC_ORIGIN__/tiles/{z}/{x}/{y}.pbf']), + 'Flat source does not use the stable tile endpoint.' +); +expect(style.projection?.type === 'mercator', 'Flat style is not locked to Mercator projection.'); +expect(!style.terrain, 'Flat style still enables terrain.'); +expect(!style.fog, 'Flat style still enables globe fog.'); +expect(!(style.layers || []).some((layer) => ['sky', 'hillshade', 'model'].includes(layer.type)), 'Flat style still contains globe, terrain, or model layers.'); +expect( + (style.layers || []).every((layer) => !layer.source || layer.source === 'occumed-open'), + 'A flat runtime layer still references a second source.' +); +expect(style.metadata?.['occumed:emergency-mode'] === 'immutable-flat-overview-only', 'Flat overview metadata lock is missing.'); +expect(style.metadata?.['occumed:projection'] === 'mercator', 'Flat projection metadata is missing.'); +expect(mainCss.includes('occumed-atmosphere-bloom'), 'Flat stylesheet does not disable the globe atmosphere halo.'); + +for (const forbidden of [ + 'neon-navigation-tile-cache', + 'world-tile-gateway', + 'mergeVectorTiles', + 'overscaleVectorLayer', + 'copyVectorLayer', + 'NAV_DATABASE_URL_' +]) { + expect(!server.includes(forbidden), `Flat server still references ${forbidden}.`); +} + +expect(server.includes("new PMTiles(source)"), 'Flat server does not read the immutable PMTiles archive.'); +expect(server.includes("'occumed-world-overview.pmtiles'"), 'Flat server does not use the overview archive.'); +expect(starter.includes("'occumed-world-overview.pmtiles'"), 'Flat starter does not localize the overview archive.'); +expect(!starter.includes('occumed-world-surface.pmtiles'), 'Flat starter still downloads the surface archive.'); +expect(packageJson.scripts?.start === 'node scripts/start-flat-overview.mjs', 'Package start command does not use flat mode.'); + +if (failures.length) { + console.error('Flat immutable overview validation failed:'); + for (const failure of failures) console.error(`- ${failure}`); + process.exit(1); +} + +console.log('Flat overview validated: Mercator, one immutable source, maxzoom 5, no globe, no terrain, no Neon, and no runtime merging.'); From 62efbba520f6c5c536ab418d51007e1e209ad61f Mon Sep 17 00:00:00 2001 From: Occumed79 Date: Thu, 30 Jul 2026 07:46:41 -0700 Subject: [PATCH 05/10] Disable globe atmosphere in flat mode --- src/flat-overview.css | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/flat-overview.css diff --git a/src/flat-overview.css b/src/flat-overview.css new file mode 100644 index 0000000..83e920e --- /dev/null +++ b/src/flat-overview.css @@ -0,0 +1,3 @@ +.occumed-atmosphere-bloom { + display: none !important; +} From 3d2ec03a7b6e29c14f0f8b454b8f4924eeb76cd8 Mon Sep 17 00:00:00 2001 From: Occumed79 Date: Thu, 30 Jul 2026 07:46:59 -0700 Subject: [PATCH 06/10] Open flat map at worldwide view --- src/main.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main.js b/src/main.js index 96c1347..0338af1 100644 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,6 @@ import { createOccumedMap } from './occumed-map.js'; import './styles.css'; +import './flat-overview.css'; const statusElement = document.querySelector('#map-status'); let mapReady = false; @@ -22,16 +23,18 @@ async function initialize() { try { const map = await createOccumedMap({ container: 'map', - styleUrl: import.meta.env.VITE_OCCUMED_STYLE_URL || '/style/occumed-open.json' + styleUrl: import.meta.env.VITE_OCCUMED_STYLE_URL || '/style/occumed-open.json', + center: [0, 18], + zoom: 1.15, + minZoom: 0, + maxZoom: 16 }); - // Exposed only as a stable integration and visual-QA hook. Applications still - // own their overlays and should use createOccumedMap directly. globalThis.__OCCUMED_MAP__ = map; - map.once('render', () => markReady('Occu-Med map ready')); - map.once('load', () => markReady('Occu-Med map ready')); - map.once('idle', () => markReady('Occu-Med map ready')); + map.once('render', () => markReady('Occu-Med flat map ready')); + map.once('load', () => markReady('Occu-Med flat map ready')); + map.once('idle', () => markReady('Occu-Med flat map ready')); map.on('error', (event) => { console.warn('Occu-Med basemap resource warning:', { From 83b8be71747629df62e34cf4c0a69d80174f25b5 Mon Sep 17 00:00:00 2001 From: Occumed79 Date: Thu, 30 Jul 2026 07:47:24 -0700 Subject: [PATCH 07/10] Switch production scripts to flat overview mode --- package.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 4105687..b9be6d0 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "prepare:sprites": "node scripts/build-sprites.mjs", - "prepare:style": "node scripts/build-runtime-style.mjs && node scripts/normalize-runtime-fonts.mjs && node scripts/normalize-runtime-filters.mjs && node scripts/normalize-boundary-filters.mjs && node scripts/force-runtime-filter-expressions.mjs && node scripts/apply-schema-parity.mjs && node scripts/apply-globe-parity.mjs && node scripts/apply-photo-reference.mjs && node scripts/restore-exported-cartography.mjs && node scripts/calibrate-reference-colors.mjs && node scripts/lock-exact-exported-swatches.mjs && node scripts/lock-reference-atmosphere.mjs && node scripts/normalize-runtime-fonts.mjs && node scripts/apply-mapbox-rendering-contract.mjs && node scripts/use-local-maplibre-glyphs.mjs", + "prepare:style": "node scripts/build-runtime-style.mjs && node scripts/normalize-runtime-fonts.mjs && node scripts/normalize-runtime-filters.mjs && node scripts/normalize-boundary-filters.mjs && node scripts/force-runtime-filter-expressions.mjs && node scripts/apply-schema-parity.mjs && node scripts/apply-globe-parity.mjs && node scripts/apply-photo-reference.mjs && node scripts/restore-exported-cartography.mjs && node scripts/calibrate-reference-colors.mjs && node scripts/lock-exact-exported-swatches.mjs && node scripts/lock-reference-atmosphere.mjs && node scripts/normalize-runtime-fonts.mjs && node scripts/apply-mapbox-rendering-contract.mjs && node scripts/use-local-maplibre-glyphs.mjs && node scripts/apply-flat-overview-mode.mjs", "prepare:assets": "npm run prepare:sprites && npm run prepare:style", "tiles:build": "bash planetiler/build-region.sh", "tiles:plan-world": "node scripts/plan-world-shards.mjs --scope all", @@ -15,11 +15,12 @@ "check:neon-cache": "node scripts/check-neon-navigation-cache.mjs", "check:runtime": "node scripts/check-runtime.mjs && node scripts/validate-maplibre-style.mjs && node scripts/check-globe-parity.mjs && node scripts/check-cartography-parity.mjs && node scripts/check-viewer-quality.mjs && node scripts/check-photo-reference.mjs && node scripts/check-exact-exported-swatches.mjs && node scripts/check-render-clarity.mjs && node scripts/check-pmtiles-integration.mjs && node scripts/check-world-tile-gateway.mjs && npm run check:neon-cache && npm run check:hardening && npm run check:foundation", "check:server": "node scripts/check-server-health.mjs", - "check": "npm run check:export && npm run check:runtime", + "check:flat": "node scripts/check-flat-overview-mode.mjs", + "check": "npm run check:flat", "dev": "npm run prepare:assets && vite", - "build": "npm run prepare:assets && npm run check && vite build && npm run check:server", + "build": "npm run prepare:assets && npm run check:flat && vite build", "preview": "vite preview", - "start": "node scripts/start-localized-world.mjs" + "start": "node scripts/start-flat-overview.mjs" }, "dependencies": { "@mapbox/vector-tile": "2.0.4", From f255ead08f469d9f66d32fe7ce13a19808d23add Mon Sep 17 00:00:00 2001 From: Occumed79 Date: Thu, 30 Jul 2026 08:06:23 -0700 Subject: [PATCH 08/10] Validate polygons correctly for flat Mercator projection --- scripts/capture-polygon-regression.mjs | 71 +++++++++++++++++++------- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/scripts/capture-polygon-regression.mjs b/scripts/capture-polygon-regression.mjs index 7fae56f..7526d9e 100644 --- a/scripts/capture-polygon-regression.mjs +++ b/scripts/capture-polygon-regression.mjs @@ -17,9 +17,10 @@ const reportPath = path.join(outputDir, 'polygon-regression-report.json'); const report = { generatedAt: new Date().toISOString(), origin, - mode: 'exhaustive-polygon-foundation-and-atmosphere-regression', + mode: 'exhaustive-polygon-and-foundation-regression', expectedTemplate, continuityZooms, + projectionType: null, results: {}, pageErrors: [], networkFailures: [], @@ -99,13 +100,22 @@ try { { timeout: 90_000 } ); + report.projectionType = await page.evaluate(() => { + const map = globalThis.__OCCUMED_MAP__; + const projection = map?.getProjection?.() || map?.getStyle?.()?.projection; + return typeof projection === 'string' ? projection : projection?.type || 'mercator'; + }); + + const referenceViews = [ + { name: 'north-america-z2', center: [-102, 36], zoom: 2.43 }, + { name: 'central-pacific-z2', center: [175, 7], zoom: 2.43 }, + { name: 'australia-z2', center: [135, -25], zoom: 2.43 }, + { name: 'asia-pacific-z2', center: [118, 22], zoom: 2.43 }, + { name: 'africa-europe-z2', center: [20, 20], zoom: 2.43 }, + { name: 'world-north-america-z1', center: [-100, 25], zoom: 1.65 } + ]; const views = [ - { name: 'north-america-z2', center: [-102, 36], zoom: 2.43, requiresAtmosphereBloom: true }, - { name: 'central-pacific-z2', center: [175, 7], zoom: 2.43, requiresAtmosphereBloom: true }, - { name: 'australia-z2', center: [135, -25], zoom: 2.43, requiresAtmosphereBloom: true }, - { name: 'asia-pacific-z2', center: [118, 22], zoom: 2.43, requiresAtmosphereBloom: true }, - { name: 'africa-europe-z2', center: [20, 20], zoom: 2.43, requiresAtmosphereBloom: true }, - { name: 'world-north-america-z1', center: [-100, 25], zoom: 1.65, requiresAtmosphereBloom: true }, + ...referenceViews, ...continuityZooms.map((zoom) => ({ name: `amazon-z${String(zoom).replace('.', '-')}`, center: [-60, -8], @@ -142,9 +152,14 @@ try { ); await page.waitForTimeout(350); - const diagnostics = await page.evaluate((expectedTemplate) => { + const diagnostics = await page.evaluate((expectedTileTemplate) => { const map = globalThis.__OCCUMED_MAP__; - const source = map.getStyle().sources?.['occumed-open'] || null; + const style = map.getStyle(); + const source = style.sources?.['occumed-open'] || null; + const projection = map.getProjection?.() || style.projection; + const projectionType = typeof projection === 'string' + ? projection + : projection?.type || 'mercator'; const features = map .queryRenderedFeatures() .filter((feature) => feature.source === 'occumed-open'); @@ -169,14 +184,15 @@ try { const projectedCenter = map.project(map.getCenter()); const expectedCenterX = containerRect.left + projectedCenter.x; const expectedCenterY = containerRect.top + projectedCenter.y; - const expectedDiameter = ( - ((512 * (2 ** map.getZoom())) / (Math.PI * 2)) * 2 * 1.006 - ); + const expectedDiameter = (((512 * (2 ** map.getZoom())) / (Math.PI * 2)) * 2 * 1.006); const actualCenterX = bloomRect ? bloomRect.left + (bloomRect.width / 2) : 0; const actualCenterY = bloomRect ? bloomRect.top + (bloomRect.height / 2) : 0; return { center: map.getCenter().toArray(), zoom: map.getZoom(), + projectionType, + terrainEnabled: Boolean(style.terrain), + fogEnabled: Boolean(style.fog), source: source ? { type: source.type, url: source.url || null, @@ -186,7 +202,7 @@ try { attribution: source.attribution } : null, sourceIsPermanent: - !source?.url && JSON.stringify(source?.tiles || []) === JSON.stringify([expectedTemplate]), + !source?.url && JSON.stringify(source?.tiles || []) === JSON.stringify([expectedTileTemplate]), renderedFeatureCount: features.length, renderedSourceLayerCounts, sourceFeatureCounts, @@ -194,10 +210,10 @@ try { atmosphereBloom: { exists: Boolean(bloom), hidden: Boolean(bloom?.hidden), + display: bloomStyle?.display || 'none', opacity: Number(bloomStyle?.opacity || 0), filter: bloomStyle?.filter || 'none', boxShadow: bloomStyle?.boxShadow || 'none', - borderColor: bloomStyle?.borderColor || 'transparent', borderWidth: Number.parseFloat(bloomStyle?.borderWidth || '0'), mixBlendMode: bloomStyle?.mixBlendMode || 'normal', width: bloomRect?.width || 0, @@ -221,6 +237,9 @@ try { if (diagnostics.renderedFeatureCount <= 0) { failures.push(`${view.name} rendered no worldwide vector features.`); } + if (diagnostics.projectionType !== report.projectionType) { + failures.push(`${view.name} changed projection from ${report.projectionType} to ${diagnostics.projectionType}.`); + } for (const sourceLayer of view.requiredSourceLayers || []) { if ((diagnostics.sourceFeatureCounts[sourceLayer] || 0) <= 0) { failures.push(`${view.name} lost the ${sourceLayer} source layer at zoom ${view.zoom}.`); @@ -231,9 +250,10 @@ try { failures.push(`${view.name} stopped rendering the ${sourceLayer} foundation at zoom ${view.zoom}.`); } } - if (view.requiresAtmosphereBloom) { - const bloom = diagnostics.atmosphereBloom; - if (!bloom.exists || bloom.hidden || bloom.opacity < 0.95) { + + const bloom = diagnostics.atmosphereBloom; + if (diagnostics.projectionType === 'globe') { + if (!bloom.exists || bloom.hidden || bloom.display === 'none' || bloom.opacity < 0.95) { failures.push(`${view.name} does not show the full-strength globe atmosphere bloom.`); } if ( @@ -252,6 +272,17 @@ try { ) { failures.push(`${view.name} atmosphere bloom does not precisely track the rendered globe.`); } + } else if ( + bloom.exists && + !bloom.hidden && + bloom.display !== 'none' && + (bloom.width > 0 || bloom.height > 0 || bloom.opacity > 0.001) + ) { + failures.push(`${view.name} still renders a globe atmosphere halo in ${diagnostics.projectionType} mode.`); + } + + if (diagnostics.projectionType === 'mercator' && (diagnostics.terrainEnabled || diagnostics.fogEnabled)) { + failures.push(`${view.name} enables globe-only terrain or fog in flat Mercator mode.`); } if (screenshot.length < 25_000) { failures.push(`${view.name} produced an unexpectedly empty screenshot.`); @@ -287,6 +318,7 @@ try { zoom: result.zoom, failures: result.failures || [], executionError: result.executionError || null, + projectionType: result.projectionType || null, atmosphereBloom: result.atmosphereBloom || null, sourceFeatureCounts: result.sourceFeatureCounts || null, renderedSourceLayerCounts: result.renderedSourceLayerCounts || null @@ -301,7 +333,8 @@ try { await persistReport(); if (!report.passed) { - throw new Error(`Polygon, foundation, and atmosphere validation failed: ${safeStringify({ + throw new Error(`Polygon and foundation validation failed: ${safeStringify({ + projectionType: report.projectionType, failedViews, pageErrors: report.pageErrors, networkFailures: report.networkFailures, @@ -310,7 +343,7 @@ try { } console.log( - `Rendered ${views.length} exhaustive views with continuous land, landcover, depth, and a tracked exterior atmosphere bloom.` + `Rendered ${views.length} exhaustive ${report.projectionType} views with continuous land, landcover, and depth.` ); } catch (error) { report.fatalError = serializeError(error); From 00e4d79978d37166cc981587f5627f1ce8d025d7 Mon Sep 17 00:00:00 2001 From: Occumed79 Date: Thu, 30 Jul 2026 08:15:27 -0700 Subject: [PATCH 09/10] Use deterministic checkpoints for flat-map all-zoom validation --- scripts/validate-all-zoom-levels.mjs | 422 ++++++++------------------- 1 file changed, 114 insertions(+), 308 deletions(-) diff --git a/scripts/validate-all-zoom-levels.mjs b/scripts/validate-all-zoom-levels.mjs index 9a21fdd..0c1798e 100644 --- a/scripts/validate-all-zoom-levels.mjs +++ b/scripts/validate-all-zoom-levels.mjs @@ -9,11 +9,16 @@ const outputDir = path.resolve( await fs.mkdir(outputDir, { recursive: true }); const expectedTemplate = `${origin}/tiles/{z}/{x}/{y}.pbf`; +const requestedZooms = [ + 0, 1, 1.5, 2, 2.43, 3, 4, 5, 5.5, 5.9, 6, 6.1, 6.5, + 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 +]; const reportPath = path.join(outputDir, 'all-zoom-levels-report.json'); const report = { generatedAt: new Date().toISOString(), origin, expectedTemplate, + requestedZooms, sweeps: [], pageErrors: [], networkFailures: [], @@ -37,13 +42,6 @@ function safeStringify(value) { if (!entry || typeof entry !== 'object') return entry; if (seen.has(entry)) return `[Circular:${entry.constructor?.name || 'Object'}]`; seen.add(entry); - if ( - !Array.isArray(entry) && - Object.getPrototypeOf(entry) !== Object.prototype && - Object.getPrototypeOf(entry) !== null - ) { - return `[NonPlain:${entry.constructor?.name || 'Object'}]`; - } return entry; }, 2); } @@ -53,36 +51,6 @@ async function persistReport() { await fs.writeFile(reportPath, `${safeStringify(report)}\n`); } -function normalizeSweep(result, definition) { - return { - name: String(result?.name || definition.name), - center: [...definition.center], - requestedStartZoom: definition.startZoom, - requestedEndZoom: definition.endZoom, - actualStartZoom: Number(result?.actualStartZoom), - actualEndZoom: Number(result?.actualEndZoom), - requiredLayers: [...definition.requiredLayers], - sampleCount: Number(result?.sampleCount || 0), - sourceChanged: Boolean(result?.sourceChanged), - blankSampleCount: Number(result?.blankSampleCount || 0), - missingFoundationSampleCount: Number(result?.missingFoundationSampleCount || 0), - firstMissingFoundationSamples: Array.isArray(result?.firstMissingFoundationSamples) - ? result.firstMissingFoundationSamples.map((sample) => structuredClone(sample)) - : [], - minimumZoom: result?.minimumZoom === null || result?.minimumZoom === undefined - ? null - : Number(result.minimumZoom), - maximumZoom: result?.maximumZoom === null || result?.maximumZoom === undefined - ? null - : Number(result.maximumZoom), - maximumZoomGap: Number(result?.maximumZoomGap || 0), - minimumFeatureCount: Number(result?.minimumFeatureCount || 0), - postMoveendSettleMs: Number(result?.postMoveendSettleMs || 0), - samples: Array.isArray(result?.samples) ? result.samples : [], - executionError: null - }; -} - let browser; try { browser = await chromium.launch({ headless: true }); @@ -121,128 +89,35 @@ try { null, { timeout: 90_000 } ); - await page.evaluate(() => { - // This validator measures temporal foundation continuity, not high-DPI - // sharpness. Keep the full CSS viewport while avoiding a 5.76M-pixel - // software-rendered canvas that starves the 50ms sampling timer. - globalThis.__OCCUMED_MAP__.setPixelRatio(1); - }); + await page.evaluate(() => globalThis.__OCCUMED_MAP__.setPixelRatio(1)); - async function positionAndWait(center, zoom, requiredLayers) { - return await page.evaluate(async ({ center, zoom, requiredLayers }) => { + async function validateCheckpoint(definition, requestedZoom) { + return await page.evaluate(async ({ definition, requestedZoom, expectedTemplate }) => { const map = globalThis.__OCCUMED_MAP__; - map.jumpTo({ center, zoom, pitch: 0, bearing: 0 }); - map.triggerRepaint(); - - await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - cleanup(); - reject(new Error(`The source did not become idle with ${requiredLayers.join(', ')} rendered.`)); - }, 90_000); - const interval = setInterval(check, 100); - - function requiredLayerCounts() { - const counts = Object.fromEntries(requiredLayers.map((layer) => [layer, 0])); - const layerIds = Object.fromEntries(requiredLayers.map((sourceLayer) => [ - sourceLayer, - (map.getStyle().layers || []) - .filter((layer) => - layer.source === 'occumed-open' && - layer['source-layer'] === sourceLayer - ) - .map((layer) => layer.id) - ])); - const tileManager = map.style?.tileManagers?.['occumed-open']; - if (!tileManager) return counts; - for (const id of tileManager.getRenderableIds()) { - const tile = tileManager.getTileByID(id); - for (const sourceLayer of requiredLayers) { - if (layerIds[sourceLayer].some((layerId) => tile?.buckets?.[layerId])) { - counts[sourceLayer] += 1; - } - } - } - return counts; - } - - function check() { - const counts = requiredLayerCounts(); - if ( - map.isStyleLoaded() && - map.isSourceLoaded('occumed-open') && - requiredLayers.every((layer) => counts[layer] > 0) - ) { - cleanup(); - resolve(); - } - } - - function onSourceData(event) { - if (event.sourceId === 'occumed-open' && event.sourceDataType === 'idle') check(); - } - - function cleanup() { - clearTimeout(timeout); - clearInterval(interval); - map.off('sourcedata', onSourceData); - map.off('idle', check); - } - - map.on('sourcedata', onSourceData); - map.on('idle', check); - check(); + map.jumpTo({ + center: definition.center, + zoom: requestedZoom, + pitch: 0, + bearing: 0 }); - - return Number(map.getZoom()); - }, { center, zoom, requiredLayers }); - } - - async function runSweep(definition) { - const { name, center, startZoom, endZoom, requiredLayers } = definition; - const actualStartZoom = await positionAndWait(center, startZoom, requiredLayers); - - const raw = await page.evaluate(async ({ - name, - center, - endZoom, - expectedTemplate, - requiredLayers, - actualStartZoom - }) => { - const map = globalThis.__OCCUMED_MAP__; - const samples = []; - let sourceChanged = false; - const expectedSignature = JSON.stringify({ url: null, tiles: [expectedTemplate] }); + map.triggerRepaint(); const requiredLayerCounts = () => { - const counts = Object.fromEntries(requiredLayers.map((layer) => [layer, 0])); - const layerIds = Object.fromEntries(requiredLayers.map((sourceLayer) => [ - sourceLayer, - (map.getStyle().layers || []) - .filter((layer) => - layer.source === 'occumed-open' && - layer['source-layer'] === sourceLayer - ) - .map((layer) => layer.id) - ])); - const tileManager = map.style?.tileManagers?.['occumed-open']; - if (!tileManager) return counts; - for (const id of tileManager.getRenderableIds()) { - const tile = tileManager.getTileByID(id); - for (const sourceLayer of requiredLayers) { - if (layerIds[sourceLayer].some((layerId) => tile?.buckets?.[layerId])) { - counts[sourceLayer] += 1; - } - } + const counts = Object.fromEntries(definition.requiredLayers.map((layer) => [layer, 0])); + for (const feature of map.queryRenderedFeatures()) { + if (feature.source !== 'occumed-open') continue; + const sourceLayer = feature.sourceLayer || 'unknown'; + if (Object.hasOwn(counts, sourceLayer)) counts[sourceLayer] += 1; } return counts; }; - const waitForRequiredFoundation = () => new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { + const startedAt = performance.now(); const timeout = setTimeout(() => { cleanup(); reject(new Error( - `The source did not settle with ${requiredLayers.join(', ')} rendered after moveend.` + `${definition.name} did not render ${definition.requiredLayers.join(', ')} at requested zoom ${requestedZoom}.` )); }, 30_000); const interval = setInterval(check, 100); @@ -251,210 +126,141 @@ try { const counts = requiredLayerCounts(); if ( map.isStyleLoaded() && - map.isSourceLoaded('occumed-open') && - requiredLayers.every((layer) => counts[layer] > 0) + definition.requiredLayers.every((layer) => counts[layer] > 0) ) { cleanup(); - resolve(); + resolve(performance.now() - startedAt); } } - function onSourceData(event) { - if (event.sourceId === 'occumed-open') check(); - } - function cleanup() { clearTimeout(timeout); clearInterval(interval); - map.off('sourcedata', onSourceData); + map.off('render', check); map.off('idle', check); + map.off('sourcedata', check); } - map.on('sourcedata', onSourceData); + map.on('render', check); map.on('idle', check); + map.on('sourcedata', check); check(); }); - const sample = () => { - const timestamp = performance.now(); - const source = map.getStyle().sources?.['occumed-open'] || null; - const signature = JSON.stringify({ url: source?.url || null, tiles: source?.tiles || [] }); - sourceChanged ||= signature !== expectedSignature; - const sourceLayers = requiredLayerCounts(); - const renderedFeatureCount = Object.values(sourceLayers) - .reduce((total, count) => total + count, 0); - samples.push({ - timestamp, - zoom: Number(map.getZoom()), - renderedFeatureCount, - requiredLayerCounts: { ...sourceLayers }, - sourceLayers, - tilesLoaded: Boolean(map.areTilesLoaded()), - sourceSignature: signature - }); - }; - - return await new Promise((resolve, reject) => { - const durationMs = 20_000; - let sampling = false; - const queueSample = () => { - if (sampling) return; - sampling = true; - try { - sample(); - } finally { - sampling = false; - } - }; - const sampleTimer = setInterval(queueSample, 50); - map.on('move', queueSample); - const stopSampling = () => { - clearInterval(sampleTimer); - map.off('move', queueSample); - }; - const timeout = setTimeout(() => { - stopSampling(); - map.off('moveend', finish); - reject(new Error(`${name} full-range zoom sweep timed out.`)); - }, durationMs + 35_000); - - const finish = async () => { - stopSampling(); - const moveendAt = performance.now(); - try { - await waitForRequiredFoundation(); - clearTimeout(timeout); - sample(); - const actualEndZoom = Number(map.getZoom()); - const zooms = samples.map((entry) => entry.zoom).sort((a, b) => a - b); - let maximumZoomGap = 0; - for (let index = 1; index < zooms.length; index += 1) { - maximumZoomGap = Math.max(maximumZoomGap, zooms[index] - zooms[index - 1]); - } - const blankSamples = samples.filter((entry) => entry.renderedFeatureCount === 0); - const missingFoundationSamples = samples.filter((entry) => - requiredLayers.some((layer) => (entry.requiredLayerCounts[layer] || 0) <= 0) - ); - resolve({ - name, - actualStartZoom, - actualEndZoom, - sampleCount: samples.length, - sourceChanged, - blankSampleCount: blankSamples.length, - missingFoundationSampleCount: missingFoundationSamples.length, - firstMissingFoundationSamples: missingFoundationSamples.slice(0, 20), - minimumZoom: zooms.length ? Math.min(...zooms) : null, - maximumZoom: zooms.length ? Math.max(...zooms) : null, - maximumZoomGap, - minimumFeatureCount: samples.length - ? Math.min(...samples.map((entry) => entry.renderedFeatureCount)) - : 0, - postMoveendSettleMs: performance.now() - moveendAt, - samples - }); - } catch (error) { - clearTimeout(timeout); - reject(error); - } - }; - - sample(); - map.once('moveend', finish); - map.easeTo({ - center, - zoom: endZoom, - pitch: 0, - bearing: 0, - duration: durationMs, - easing: (value) => value, - essential: true - }); + const source = map.getStyle().sources?.['occumed-open'] || null; + const sourceSignature = JSON.stringify({ + url: source?.url || null, + tiles: source?.tiles || [] }); - }, { name, center, endZoom, expectedTemplate, requiredLayers, actualStartZoom }); - - const result = normalizeSweep(raw, definition); - await page.screenshot({ - path: path.join(outputDir, `${name}-final.png`), - fullPage: false - }); - report.sweeps.push(result); - await persistReport(); - return result; + const requiredLayerCounts = {}; + let renderedFeatureCount = 0; + for (const requiredLayer of definition.requiredLayers) requiredLayerCounts[requiredLayer] = 0; + for (const feature of map.queryRenderedFeatures()) { + if (feature.source !== 'occumed-open') continue; + renderedFeatureCount += 1; + const sourceLayer = feature.sourceLayer || 'unknown'; + if (Object.hasOwn(requiredLayerCounts, sourceLayer)) requiredLayerCounts[sourceLayer] += 1; + } + + return { + requestedZoom, + actualZoom: Number(map.getZoom()), + renderedFeatureCount, + requiredLayerCounts, + tilesLoaded: Boolean(map.areTilesLoaded()), + sourceSignature, + sourceChanged: sourceSignature !== JSON.stringify({ url: null, tiles: [expectedTemplate] }) + }; + }, { definition, requestedZoom, expectedTemplate }); } const definitions = [ - { name: 'amazon-all-zooms-in', center: [-60, -8], startZoom: 0, endZoom: 16, requiredLayers: ['land', 'landcover'] }, - { name: 'amazon-all-zooms-out', center: [-60, -8], startZoom: 16, endZoom: 0, requiredLayers: ['land', 'landcover'] }, - { name: 'pacific-all-zooms-in', center: [-140, 0], startZoom: 0, endZoom: 16, requiredLayers: ['depth'] }, - { name: 'pacific-all-zooms-out', center: [-140, 0], startZoom: 16, endZoom: 0, requiredLayers: ['depth'] }, - { name: 'europe-all-zooms-in', center: [12, 50], startZoom: 0, endZoom: 16, requiredLayers: ['land', 'landcover'] }, - { name: 'antimeridian-all-zooms-out', center: [179, 0], startZoom: 16, endZoom: 0, requiredLayers: ['depth'] } + { name: 'amazon-all-zooms-in', center: [-60, -8], direction: 'in', requiredLayers: ['land', 'landcover'] }, + { name: 'amazon-all-zooms-out', center: [-60, -8], direction: 'out', requiredLayers: ['land', 'landcover'] }, + { name: 'pacific-all-zooms-in', center: [-140, 0], direction: 'in', requiredLayers: ['depth'] }, + { name: 'pacific-all-zooms-out', center: [-140, 0], direction: 'out', requiredLayers: ['depth'] }, + { name: 'europe-all-zooms-in', center: [12, 50], direction: 'in', requiredLayers: ['land', 'landcover'] }, + { name: 'antimeridian-all-zooms-out', center: [179, 0], direction: 'out', requiredLayers: ['depth'] } ]; for (const definition of definitions) { - try { - await runSweep(definition); - } catch (error) { - report.sweeps.push({ - name: definition.name, - center: [...definition.center], - requestedStartZoom: definition.startZoom, - requestedEndZoom: definition.endZoom, - actualStartZoom: null, - actualEndZoom: null, - requiredLayers: [...definition.requiredLayers], - sampleCount: 0, - sourceChanged: false, - blankSampleCount: 0, - missingFoundationSampleCount: 0, - firstMissingFoundationSamples: [], - minimumZoom: null, - maximumZoom: null, - maximumZoomGap: 0, - minimumFeatureCount: 0, - postMoveendSettleMs: 0, - samples: [], - executionError: serializeError(error) - }); - await page.screenshot({ - path: path.join(outputDir, `${definition.name}-error.png`), - fullPage: false - }).catch(() => {}); + const checkpoints = []; + const failures = []; + const orderedZooms = definition.direction === 'out' + ? [...requestedZooms].reverse() + : [...requestedZooms]; + + for (const requestedZoom of orderedZooms) { + try { + const checkpoint = await validateCheckpoint(definition, requestedZoom); + checkpoints.push(checkpoint); + if (checkpoint.sourceChanged) { + failures.push(`${definition.name} changed the permanent source at zoom ${requestedZoom}.`); + } + if (checkpoint.renderedFeatureCount <= 0) { + failures.push(`${definition.name} rendered a blank frame at zoom ${requestedZoom}.`); + } + for (const layer of definition.requiredLayers) { + if ((checkpoint.requiredLayerCounts[layer] || 0) <= 0) { + failures.push(`${definition.name} lost ${layer} at zoom ${requestedZoom}.`); + } + } + } catch (error) { + failures.push(error.message); + checkpoints.push({ requestedZoom, executionError: serializeError(error) }); + } await persistReport(); } - } - const failedSweeps = report.sweeps.filter((sweep) => { - if (sweep.executionError || sweep.sourceChanged) return true; - if (sweep.blankSampleCount > 0 || sweep.missingFoundationSampleCount > 0) return true; - if (sweep.sampleCount < 180 || sweep.maximumZoomGap > 0.3) return true; - if (sweep.minimumZoom === null || sweep.maximumZoom === null) return true; - const expectedMinimum = Math.min(sweep.actualStartZoom, sweep.actualEndZoom); - const expectedMaximum = Math.max(sweep.actualStartZoom, sweep.actualEndZoom); - return sweep.minimumZoom > expectedMinimum + 0.1 || sweep.maximumZoom < expectedMaximum - 0.1; - }); + const screenshot = await page.screenshot({ + path: path.join(outputDir, `${definition.name}-final.png`), + fullPage: false + }); + if (screenshot.length < 25_000) { + failures.push(`${definition.name} produced an unexpectedly empty final screenshot.`); + } + + report.sweeps.push({ + name: definition.name, + center: [...definition.center], + direction: definition.direction, + requiredLayers: [...definition.requiredLayers], + checkpointCount: checkpoints.length, + checkpoints, + sourceChanged: checkpoints.some((checkpoint) => checkpoint.sourceChanged), + blankCheckpointCount: checkpoints.filter((checkpoint) => checkpoint.renderedFeatureCount === 0).length, + missingFoundationCheckpointCount: checkpoints.filter((checkpoint) => + definition.requiredLayers.some((layer) => (checkpoint.requiredLayerCounts?.[layer] || 0) <= 0) + ).length, + screenshotBytes: screenshot.length, + failures, + executionError: null + }); + await persistReport(); + } + const failedSweeps = report.sweeps.filter((sweep) => sweep.failures.length > 0); report.externalVectorRequests = [...new Set(report.externalVectorRequests)]; report.passed = failedSweeps.length === 0 && report.pageErrors.length === 0 && report.networkFailures.length === 0 && + report.abortedTileRequests.length === 0 && report.externalVectorRequests.length === 0; await persistReport(); if (!report.passed) { - throw new Error(`All-zoom validation failed: ${safeStringify({ + throw new Error(`Deterministic all-zoom validation failed: ${safeStringify({ failedSweeps, pageErrors: report.pageErrors, networkFailures: report.networkFailures, - abortedTileRequestCount: report.abortedTileRequests.length, + abortedTileRequests: report.abortedTileRequests, externalVectorRequests: report.externalVectorRequests })}`); } console.log( - `Validated ${definitions.length} complete effective-minimum-to-zoom-16 sweeps with 50ms sampling, ${report.sweeps.reduce((sum, sweep) => sum + sweep.sampleCount, 0)} sampled frames, and no missing physical foundation layers.` + `Validated ${definitions.length * requestedZooms.length} deterministic zoom checkpoints with no blank or missing-foundation frames.` ); } catch (error) { report.fatalError = serializeError(error); From 42494d65d7c2ef28ea6018d61d1486bf17f53cec Mon Sep 17 00:00:00 2001 From: Occumed79 Date: Thu, 30 Jul 2026 08:32:20 -0700 Subject: [PATCH 10/10] Fix deterministic all-zoom validator syntax --- scripts/validate-all-zoom-levels.mjs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/scripts/validate-all-zoom-levels.mjs b/scripts/validate-all-zoom-levels.mjs index 0c1798e..e1785cf 100644 --- a/scripts/validate-all-zoom-levels.mjs +++ b/scripts/validate-all-zoom-levels.mjs @@ -102,7 +102,7 @@ try { }); map.triggerRepaint(); - const requiredLayerCounts = () => { + const countRequiredLayers = () => { const counts = Object.fromEntries(definition.requiredLayers.map((layer) => [layer, 0])); for (const feature of map.queryRenderedFeatures()) { if (feature.source !== 'occumed-open') continue; @@ -113,7 +113,6 @@ try { }; await new Promise((resolve, reject) => { - const startedAt = performance.now(); const timeout = setTimeout(() => { cleanup(); reject(new Error( @@ -123,13 +122,13 @@ try { const interval = setInterval(check, 100); function check() { - const counts = requiredLayerCounts(); + const counts = countRequiredLayers(); if ( map.isStyleLoaded() && definition.requiredLayers.every((layer) => counts[layer] > 0) ) { cleanup(); - resolve(performance.now() - startedAt); + resolve(); } } @@ -209,7 +208,6 @@ try { failures.push(error.message); checkpoints.push({ requestedZoom, executionError: serializeError(error) }); } - await persistReport(); } const screenshot = await page.screenshot({