Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions .github/workflows/validate-continuous-zoom.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: Validate Continuous Worldwide Zoom

on:
workflow_dispatch:
pull_request:
branches:
- main
paths:
- .github/workflows/validate-continuous-zoom.yml
- package-lock.json
- package.json
- style.json
- scripts/**
- src/**
- server.mjs

permissions:
contents: read

concurrency:
group: occumed-continuous-zoom-${{ github.ref }}
cancel-in-progress: true

env:
SURFACE_ASSET: occumed-world-surface.pmtiles

jobs:
continuous-motion:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm

- run: npm ci

- name: Build and validate the production map
run: npm run build

- name: Install Chromium
run: npx playwright install --with-deps chromium

- name: Build candidate worldwide physical surface
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential libsqlite3-dev zlib1g-dev
bash scripts/build-world-surface.sh \
"dist/virtual-assets/${SURFACE_ASSET}"

- name: Run continuous zoom and pan gate
run: |
set -euo pipefail
mkdir -p continuous-motion
OCCUMED_WORLD_SURFACE_URL="http://127.0.0.1:4173/virtual-assets/${SURFACE_ASSET}" \
node server.mjs > continuous-motion/server.log 2>&1 &
server_pid="$!"
cleanup() {
status="$?"
if [ "$status" -ne 0 ]; then
tail -300 continuous-motion/server.log || true
fi
kill "$server_pid" 2>/dev/null || true
exit "$status"
}
trap cleanup EXIT

for attempt in {1..90}; do
if curl --fail --silent http://127.0.0.1:4173/healthz > /dev/null; then
break
fi
sleep 1
done
curl --fail http://127.0.0.1:4173/healthz

OCCUMED_PREVIEW_OUTPUT=continuous-motion/results \
node scripts/validate-continuous-zoom.mjs

- uses: actions/upload-artifact@v4
if: always()
with:
name: continuous-zoom-${{ github.sha }}
path: |
continuous-motion/results
continuous-motion/server.log
if-no-files-found: error
retention-days: 14
27 changes: 20 additions & 7 deletions scripts/check-globe-parity.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,37 @@ if (runtime.projection?.type !== 'globe') fail('The reusable style must use glob
if (runtime.fog) fail('Mapbox fog must be translated instead of shipped to MapLibre unchanged.');
if (!runtime.sky) fail('The MapLibre sky/atmosphere configuration is missing.');
if (runtime.sky?.['sky-color'] !== '#181A1D') fail('The fixed reference-space hex changed.');
if (runtime.sky?.['horizon-color'] !== '#F5FDFF') fail('The atmospheric horizon hex changed.');
if (runtime.sky?.['fog-color'] !== '#B8E6FF') fail('The cool outer-atmosphere hex changed.');
if (runtime.sky?.['horizon-color'] !== 'rgba(245, 253, 255, 0.98)') {
fail('The narrow white horizon color changed.');
}
if (runtime.sky?.['fog-color'] !== 'rgba(184, 230, 255, 0.14)') {
fail('The translucent cool-blue outer glow changed.');
}
if (!runtime.sky?.['atmosphere-blend']) fail('The globe atmosphere blend is missing.');
if (runtime.light) fail('Directional global light must remain disabled to prevent rotation-dependent washout.');
if (runtime.sky?.['horizon-fog-blend'] !== 0) fail('Horizon fog must remain disabled to prevent surface washout.');
if ((runtime.sky?.['horizon-fog-blend'] ?? 1) > 0.1) {
fail('Horizon fog is broad enough to wash over the visible hemisphere.');
}
if (runtime.sky?.['fog-ground-blend'] !== 0) fail('Ground fog must remain disabled to preserve surface contrast.');

const atmosphereOutputs = expressionOutputs(runtime.sky?.['atmosphere-blend']);
if (!atmosphereOutputs.some((value) => value >= 0.65)) {
fail('The white-blue atmosphere is too weak to match the supplied glowing globe reference.');
if (!atmosphereOutputs.some((value) => value >= 0.12)) {
fail('The white-blue atmospheric edge is too weak to remain visible.');
}
if (atmosphereOutputs.some((value) => value > 0.2)) {
fail('The atmosphere extends too far across the globe surface and reads as directional sunlight.');
}
if (atmosphereOutputs.at(-1) !== 0) {
fail('The globe atmosphere does not disappear before detailed regional and city zooms.');
}

const horizonOutputs = expressionOutputs(runtime.sky?.['sky-horizon-blend']);
if (!horizonOutputs.some((value) => value >= 0.2)) {
if (!horizonOutputs.some((value) => value >= 0.03)) {
fail('The narrow luminous horizon rim is too weak.');
}
if (horizonOutputs.some((value) => value > 0.06)) {
fail('The horizon blend is too broad to remain an edge-only bloom.');
}
if (horizonOutputs.at(-1) !== 0) {
fail('The horizon rim does not fade out before detailed zooms.');
}
Expand Down Expand Up @@ -105,11 +117,12 @@ if (runtime.metadata?.['occumed:layer-specific-palette'] !== true) fail('Layer-s
if (runtime.metadata?.['occumed:raster-relief-disabled'] !== true) fail('Raster relief protection is missing.');
if (runtime.metadata?.['occumed:reference-atmosphere'] !== true) fail('The reference atmosphere pass did not run.');
if (runtime.metadata?.['occumed:atmosphere-surface-wash-disabled'] !== true) fail('Atmosphere surface-wash protection is missing.');
if (runtime.metadata?.['occumed:atmosphere-edge-only'] !== true) fail('The edge-only atmosphere protection is missing.');

if (failures.length) {
console.error('Occu-Med globe parity validation failed:');
for (const failure of failures) console.error(`- ${failure}`);
process.exit(1);
}

console.log(`Globe parity validated: dark space, strong white-blue atmosphere, layered land, clear blue water, and ${allColors.size} structure-specific colors.`);
console.log(`Globe parity validated: dark space, narrow white-blue edge bloom, neutral surface lighting, layered land, clear blue water, and ${allColors.size} structure-specific colors.`);
23 changes: 20 additions & 3 deletions scripts/check-pmtiles-integration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,18 @@ if (styleBuilder.toLowerCase().includes('openfreemap')) {
if (helper.includes('Protocol') || helper.includes('setUrl(') || helper.includes('world-pmtiles-router')) {
fail('The browser helper still contains PMTiles shard routing.');
}
if (!helper.includes('fadeDuration: 0')) fail('The browser can crossfade stale basemap tiles.');
if (!helper.includes('cancelPendingTileRequestsWhileZooming: false')) {
fail('The browser can cancel still-loading parent tiles during zoom.');
}
if (!helper.includes('maxTileCacheZoomLevels: 8')) {
fail('The browser does not retain enough parent zoom levels for seamless motion.');
}
if (!helper.includes('refreshExpiredTiles: false')) {
fail('The browser can replace visible tiles through in-session expiry refreshes.');
}
if (!helper.includes('fadeDuration: 300')) {
fail('Normal symbol collision fading is not enabled during zoom.');
}
if (!helper.includes("source.tiles = source.tiles.map")) {
fail('Permanent vector tile templates are not resolved to the style origin.');
}
Expand All @@ -66,6 +77,12 @@ if (!gateway.includes('Promise.all')) fail('The gateway cannot resolve intersect
if (!gateway.includes('mergeVectorTiles')) fail('The gateway cannot merge MVT layers at shard boundaries.');
if (!gateway.includes('MemoryTileCache')) fail('Resolved worldwide tiles are not cached.');
if (!gateway.includes('overscaleVectorLayer')) fail('The land surface cannot remain continuous above its generalized zoom.');
if (!gateway.includes("includeLayers: ['land']")) {
fail('The gateway does not isolate the worldwide surface to a non-overlapping land mask.');
}
if (gateway.includes("includeLayers: ['land', 'landcover', 'depth']")) {
fail('The gateway still overlays generalized surface detail on regional geometry.');
}
if (!server.includes('OCCUMED_WORLD_SURFACE_URL')) {
fail('Read-only visual validation cannot serve its candidate physical surface.');
}
Expand All @@ -77,7 +94,7 @@ if (!manifestBuilder.includes('version: 2')) fail('The server-only routing manif
if (!manifestBuilder.includes('overviewAsset')) fail('The manifest does not declare the consolidated overview archive.');
if (!manifestBuilder.includes('surfaceAsset')) fail('The manifest does not declare the worldwide land surface.');
if (!manifestBuilder.includes("surfaceLayers: ['land', 'landcover', 'depth']")) {
fail('The manifest does not declare continuous vector land, landcover, and bathymetry.');
fail('The manifest does not document the physical surface archive schema.');
}
if (manifestBuilder.includes('switchZoom')) fail('The obsolete browser switch zoom remains in the manifest.');
if (manifestBuilder.includes('archiveProxyTemplate')) fail('The manifest still advertises regional archives to browsers.');
Expand Down Expand Up @@ -121,4 +138,4 @@ if (failures.length) {
process.exit(1);
}

console.log('PMTiles storage integration validated behind one permanent worldwide vector endpoint.');
console.log('PMTiles storage integration validated behind one permanent worldwide vector endpoint with parent-tile retention and non-overlapping surface geometry.');
35 changes: 25 additions & 10 deletions scripts/check-world-tile-gateway.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,12 @@ const overscaled = inspectVectorTile(overscaleVectorLayer(surface, {
targetY: 32768
}));
expect(overscaled.land?.featureCount === 1, 'The worldwide land surface cannot overscale through max zoom.');
const physicalSurface = inspectVectorTile(
mergeVectorTiles([surface], { includeLayers: ['land', 'landcover', 'depth'] })
const physicalLandMask = inspectVectorTile(
mergeVectorTiles([surface], { includeLayers: ['land'] })
);
expect(physicalSurface.land?.featureCount === 1, 'The physical surface lost its land layer.');
expect(physicalSurface.landcover?.featureCount === 1, 'The physical surface lost generalized landcover.');
expect(physicalSurface.depth?.featureCount === 1, 'The physical surface lost its bathymetry layer.');
expect(physicalLandMask.land?.featureCount === 1, 'The physical surface lost its land layer.');
expect(!physicalLandMask.landcover, 'The physical surface still overlays generalized landcover on regional detail.');
expect(!physicalLandMask.depth, 'The physical surface still overlays generalized bathymetry on regional detail.');
const normalizedProperties = normalizeMvtProperties({
safeRank: 4,
unsafePositive: 2 ** 65,
Expand Down Expand Up @@ -179,7 +179,18 @@ expect(!JSON.stringify(runtime).includes('pmtiles://'), 'A storage archive is st
expect(!helper.includes('setUrl('), 'The browser helper can still replace the vector source URL.');
expect(!helper.includes("addProtocol('pmtiles'"), 'The browser still reads PMTiles storage shards directly.');
expect(!helper.includes('WorldPmtilesRouter'), 'The browser still installs the removed regional router.');
expect(helper.includes('fadeDuration: 0'), 'Tile fading can still expose stale cartography during zoom.');
expect(
helper.includes('cancelPendingTileRequestsWhileZooming: false'),
'MapLibre can still cancel parent tiles while child zoom tiles are loading.'
);
expect(
helper.includes('maxTileCacheZoomLevels: 8'),
'The browser does not retain enough parent zoom levels for continuous motion.'
);
expect(
helper.includes('fadeDuration: 300'),
'Normal symbol collision fading is not enabled during continuous zoom.'
);
expect(server.includes('/tiles\\/(\\d+)\\/(\\d+)\\/(\\d+)\\.pbf'), 'The server does not expose the permanent worldwide tile route.');
expect(server.includes("'world-virtual-manifest.json'"), 'The gateway does not use the isolated server-only manifest.');
expect(!server.includes('/world-tiles/'), 'The server still exposes regional storage URLs to the browser.');
Expand All @@ -194,13 +205,17 @@ expect(
'A missing overview enrichment can still reject the complete worldwide tile.'
);
expect(
gateway.includes("includeLayers: ['land', 'landcover', 'depth']"),
'The gateway does not expose land, landcover, and bathymetry as one continuous physical surface.'
gateway.includes("includeLayers: ['land']"),
'The gateway does not isolate the continuous physical surface to the land mask.'
);
expect(
!gateway.includes("includeLayers: ['land', 'landcover', 'depth']"),
'The gateway still overlays generalized landcover and bathymetry on regional detail.'
);
expect(manifestBuilder.includes('version: 2'), 'The server-only routing manifest is not version 2.');
expect(
manifestBuilder.includes("surfaceLayers: ['land', 'landcover', 'depth']"),
'The routing manifest does not declare the complete physical surface schema.'
'The routing manifest does not document the surface archive schema.'
);
expect(!manifestBuilder.includes('switchZoom'), 'The obsolete browser switch zoom remains in the manifest.');

Expand All @@ -217,4 +232,4 @@ if (failures.length) {
process.exit(1);
}

console.log('Virtual worldwide tileset validated: one permanent source, boundary merging, antimeridian routing, land, landcover, and bathymetry continuity, surface overscaling, caching, and no browser-visible shards.');
console.log('Virtual worldwide tileset validated: one permanent source, boundary merging, antimeridian routing, land-mask continuity, parent-tile retention, caching, and no browser-visible shards.');
30 changes: 16 additions & 14 deletions scripts/lock-reference-atmosphere.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,32 @@ const runtime = JSON.parse(await fs.readFile(runtimePath, 'utf8'));

runtime.projection = { type: 'globe' };

// Match the supplied Studio globe: dark space, a narrow white horizon edge,
// and a broader cool-blue atmospheric bloom. Ground and horizon fog remain
// disabled so the glow stays outside the globe instead of washing out the map.
// MapLibre's atmosphere is a screen-space globe effect, not a directional sun.
// Keep the map surface neutral and confine the white-blue light to a thin,
// even rim at the globe limb. High blend values push the atmosphere far across
// the visible hemisphere and create the incorrect "sunlit half-planet" wash.
runtime.sky = {
'sky-color': '#181A1D',
'horizon-color': '#F5FDFF',
'fog-color': '#B8E6FF',
'horizon-color': 'rgba(245, 253, 255, 0.98)',
'fog-color': 'rgba(184, 230, 255, 0.14)',
'sky-horizon-blend': [
'interpolate',
['linear'],
['zoom'],
0, 0.24,
2.5, 0.2,
4.5, 0.1,
0, 0.052,
2.5, 0.042,
4.5, 0.018,
6.25, 0
],
'horizon-fog-blend': 0,
'horizon-fog-blend': 0.08,
'fog-ground-blend': 0,
'atmosphere-blend': [
'interpolate',
['linear'],
['zoom'],
0, 0.84,
2.5, 0.76,
4.5, 0.38,
0, 0.18,
2.5, 0.145,
4.5, 0.06,
6.25, 0
]
};
Expand All @@ -43,10 +44,11 @@ delete runtime.light;
runtime.metadata = {
...(runtime.metadata || {}),
'occumed:reference-atmosphere': true,
'occumed:reference-atmosphere-pass': 2,
'occumed:reference-atmosphere-pass': 3,
'occumed:atmosphere-surface-wash-disabled': true,
'occumed:atmosphere-edge-only': true,
'occumed:atmosphere-fades-before-detail': true
};

await fs.writeFile(runtimePath, `${JSON.stringify(runtime, null, 2)}\n`);
console.log('Locked the luminous white-blue reference atmosphere without adding surface fog.');
console.log('Locked a narrow, even white-blue globe rim without surface wash.');
Loading
Loading