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
41 changes: 38 additions & 3 deletions .github/workflows/validate-continuous-zoom.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@ concurrency:
cancel-in-progress: true

env:
WORLD_RELEASE_TAG: occumed-world-v1
OVERVIEW_ASSET: occumed-world-overview.pmtiles
SURFACE_ASSET: occumed-world-surface.pmtiles
PMTILES_VERSION: v1.31.2

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

Expand All @@ -36,13 +39,40 @@ jobs:
node-version: 24
cache: npm

- uses: actions/setup-go@v5
with:
go-version: 1.24.x

- 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: Install pinned PMTiles converter
run: |
set -euo pipefail
go install "github.com/protomaps/go-pmtiles@${PMTILES_VERSION}"
"$(go env GOPATH)/bin/go-pmtiles" version

- name: Build corrected candidate worldwide overview
run: |
set -euo pipefail
mkdir -p candidate dist/virtual-assets
node scripts/build-world-overview.mjs \
--manifest "https://github.com/${GITHUB_REPOSITORY}/releases/download/${WORLD_RELEASE_TAG}/world-manifest.json" \
--release-base "https://github.com/${GITHUB_REPOSITORY}/releases/download/${WORLD_RELEASE_TAG}" \
--output candidate/overview-tiles \
--maxzoom 5 \
--concurrency 24
python3 scripts/pack-mbtiles.py \
--tiles candidate/overview-tiles \
--metadata candidate/overview-tiles/metadata.json \
--output candidate/overview.mbtiles
"$(go env GOPATH)/bin/go-pmtiles" convert \
candidate/overview.mbtiles \
"dist/virtual-assets/${OVERVIEW_ASSET}"
test -s "dist/virtual-assets/${OVERVIEW_ASSET}"
test "$(head -c 7 "dist/virtual-assets/${OVERVIEW_ASSET}")" = PMTiles

- name: Build candidate worldwide physical surface
run: |
Expand All @@ -53,10 +83,14 @@ jobs:
bash scripts/build-world-surface.sh \
"dist/virtual-assets/${SURFACE_ASSET}"

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

- name: Run continuous zoom and pan gate
run: |
set -euo pipefail
mkdir -p continuous-motion
OCCUMED_WORLD_OVERVIEW_URL="http://127.0.0.1:4173/virtual-assets/${OVERVIEW_ASSET}" \
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="$!"
Expand Down Expand Up @@ -88,5 +122,6 @@ jobs:
path: |
continuous-motion/results
continuous-motion/server.log
dist/virtual-assets/occumed-world-overview.pmtiles
if-no-files-found: error
retention-days: 14
131 changes: 131 additions & 0 deletions .github/workflows/validate-polygon-artifacts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
name: Validate Worldwide Polygon Geometry

on:
workflow_dispatch:
pull_request:
branches:
- main
paths:
- .github/workflows/validate-polygon-artifacts.yml
- package-lock.json
- package.json
- scripts/build-world-overview.mjs
- scripts/build-world-surface.sh
- scripts/capture-polygon-regression.mjs
- scripts/check-world-tile-gateway.mjs
- scripts/pack-mbtiles.py
- src/server/**
- server.mjs

permissions:
contents: read

concurrency:
group: occumed-polygon-regression-${{ github.ref }}
cancel-in-progress: true

env:
WORLD_RELEASE_TAG: occumed-world-v1
OVERVIEW_ASSET: occumed-world-overview.pmtiles
SURFACE_ASSET: occumed-world-surface.pmtiles
PMTILES_VERSION: v1.31.2

jobs:
rebuilt-overview:
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- uses: actions/checkout@v4

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

- uses: actions/setup-go@v5
with:
go-version: 1.24.x

- run: npm ci

- name: Build and run unit guards
run: npm run build

- name: Install pinned PMTiles converter
run: |
set -euo pipefail
go install "github.com/protomaps/go-pmtiles@${PMTILES_VERSION}"
"$(go env GOPATH)/bin/go-pmtiles" version

- name: Rebuild corrected zoom 0-5 overview
run: |
set -euo pipefail
mkdir -p candidate dist/virtual-assets
node scripts/build-world-overview.mjs \
--manifest "https://github.com/${GITHUB_REPOSITORY}/releases/download/${WORLD_RELEASE_TAG}/world-manifest.json" \
--release-base "https://github.com/${GITHUB_REPOSITORY}/releases/download/${WORLD_RELEASE_TAG}" \
--output candidate/overview-tiles \
--maxzoom 5 \
--concurrency 24
python3 scripts/pack-mbtiles.py \
--tiles candidate/overview-tiles \
--metadata candidate/overview-tiles/metadata.json \
--output candidate/overview.mbtiles
"$(go env GOPATH)/bin/go-pmtiles" convert \
candidate/overview.mbtiles \
"dist/virtual-assets/${OVERVIEW_ASSET}"
test -s "dist/virtual-assets/${OVERVIEW_ASSET}"
test "$(head -c 7 "dist/virtual-assets/${OVERVIEW_ASSET}")" = PMTiles

- name: Build candidate worldwide land 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}"
test -s "dist/virtual-assets/${SURFACE_ASSET}"

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

- name: Render polygon regression globe views
run: |
set -euo pipefail
mkdir -p polygon-regression
OCCUMED_WORLD_OVERVIEW_URL="http://127.0.0.1:4173/virtual-assets/${OVERVIEW_ASSET}" \
OCCUMED_WORLD_SURFACE_URL="http://127.0.0.1:4173/virtual-assets/${SURFACE_ASSET}" \
node server.mjs > polygon-regression/server.log 2>&1 &
server_pid="$!"
cleanup() {
status="$?"
if [ "$status" -ne 0 ]; then
tail -300 polygon-regression/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=polygon-regression/results \
node scripts/capture-polygon-regression.mjs

- uses: actions/upload-artifact@v4
if: always()
with:
name: polygon-regression-${{ github.sha }}
path: |
polygon-regression/results
polygon-regression/server.log
dist/virtual-assets/occumed-world-overview.pmtiles
if-no-files-found: error
retention-days: 14
158 changes: 158 additions & 0 deletions scripts/capture-polygon-regression.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { chromium } from 'playwright';

const origin = (process.env.OCCUMED_PREVIEW_ORIGIN || 'http://127.0.0.1:4173').replace(/\/$/, '');
const outputDir = path.resolve(
process.env.OCCUMED_PREVIEW_OUTPUT || 'visual-validation/polygon-regression'
);
await fs.mkdir(outputDir, { recursive: true });

const expectedTemplate = `${origin}/tiles/{z}/{x}/{y}.pbf`;
const browser = await chromium.launch({ headless: true });

try {
const context = await browser.newContext({
viewport: { width: 1440, height: 1000 },
deviceScaleFactor: 2,
colorScheme: 'dark'
});
const page = await context.newPage();
const pageErrors = [];
const networkFailures = [];
const externalVectorRequests = [];

page.on('pageerror', (error) => pageErrors.push(error.message));
page.on('request', (request) => {
const url = request.url();
if (/\.pbf(?:$|\?)/i.test(url) && !url.startsWith(`${origin}/tiles/`)) {
externalVectorRequests.push(url);
}
});
page.on('requestfailed', (request) => {
networkFailures.push({
type: 'requestfailed',
url: request.url(),
error: request.failure()?.errorText || 'unknown request failure'
});
});
page.on('response', (response) => {
if (response.status() >= 400) {
networkFailures.push({
type: 'http',
url: response.url(),
status: response.status()
});
}
});

await page.goto(origin, { waitUntil: 'domcontentloaded', timeout: 90_000 });
await page.waitForFunction(
() => globalThis.__OCCUMED_MAP__?.isStyleLoaded(),
null,
{ timeout: 90_000 }
);

const views = [
{ 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 results = {};

for (const view of views) {
await page.evaluate(({ center, zoom }) => {
const map = globalThis.__OCCUMED_MAP__;
map.jumpTo({ center, zoom, pitch: 0, bearing: 0 });
map.triggerRepaint();
}, view);

await page.waitForFunction(
() => {
const map = globalThis.__OCCUMED_MAP__;
return map?.isStyleLoaded() && map.areTilesLoaded() &&
map.queryRenderedFeatures().some((feature) => feature.source === 'occumed-open');
},
null,
{ timeout: 90_000 }
);
await page.waitForTimeout(500);

const diagnostics = await page.evaluate((expectedTemplate) => {
const map = globalThis.__OCCUMED_MAP__;
const source = map.getStyle().sources?.['occumed-open'] || null;
const features = map
.queryRenderedFeatures()
.filter((feature) => feature.source === 'occumed-open');
const sourceLayerCounts = {};
const styleLayerCounts = {};
for (const feature of features) {
const sourceLayer = feature.sourceLayer || 'unknown';
const styleLayer = feature.layer?.id || 'unknown';
sourceLayerCounts[sourceLayer] = (sourceLayerCounts[sourceLayer] || 0) + 1;
styleLayerCounts[styleLayer] = (styleLayerCounts[styleLayer] || 0) + 1;
}
return {
center: map.getCenter().toArray(),
zoom: map.getZoom(),
source,
sourceIsPermanent:
!source?.url && JSON.stringify(source?.tiles || []) === JSON.stringify([expectedTemplate]),
renderedFeatureCount: features.length,
sourceLayerCounts,
styleLayerCounts
};
}, expectedTemplate);

if (!diagnostics.sourceIsPermanent) {
throw new Error(`${view.name} changed the permanent vector source.`);
}
if (diagnostics.renderedFeatureCount <= 0) {
throw new Error(`${view.name} rendered no worldwide vector features.`);
}

const screenshot = await page.screenshot({
path: path.join(outputDir, `${view.name}.png`),
fullPage: false
});
if (screenshot.length < 25_000) {
throw new Error(`${view.name} produced an unexpectedly empty screenshot.`);
}
results[view.name] = { ...diagnostics, screenshotBytes: screenshot.length };
}

const report = {
generatedAt: new Date().toISOString(),
origin,
mode: 'rebuilt-overview-polygon-regression',
expectedTemplate,
results,
pageErrors,
networkFailures,
externalVectorRequests: [...new Set(externalVectorRequests)],
passed:
pageErrors.length === 0 &&
networkFailures.length === 0 &&
externalVectorRequests.length === 0
};

await fs.writeFile(
path.join(outputDir, 'polygon-regression-report.json'),
`${JSON.stringify(report, null, 2)}\n`
);

if (!report.passed) {
throw new Error(`Polygon regression validation failed: ${JSON.stringify({
pageErrors,
networkFailures,
externalVectorRequests: report.externalVectorRequests
})}`);
}

console.log(`Rendered ${views.length} rebuilt-overview globe views without browser or network errors.`);
} finally {
await browser.close();
}
Loading
Loading