diff --git a/README.ja.md b/README.ja.md index 5e0f5a1..587f506 100644 --- a/README.ja.md +++ b/README.ja.md @@ -225,6 +225,14 @@ proj-wasm 0.1.0-alpha8 は PROJ object を解放してもメモリを全部は - 実 demo: `demo/crs-candidate-lookup.html` - GitHub Pages: `https://code4history.github.io/Atlas/` +デモ画面には 3 つの対比のプリセットがあり、それぞれ A と B の 2 ボタンを持ちます。 + +- `place`: 同じ設定のまま京都(A)と東京(B)の範囲を比べる。候補の平面直角座標系が変わり、片方は VI 系、もう片方は VIII・IX 系になる。 +- `historical-grid`: 京都の範囲で、歴史グリッドの投影を外した場合(A)と入れた場合(B)を比べる。B にだけ `UTJCP:ZONE-B` が加わり、件数が 1 つ増える。 +- `area-filter`: 京都の範囲で、地域を Japan に絞った場合(A)と絞らない場合(B)を比べる。絞らないと日本以外の CRS も一覧に並ぶ。 + +プリセットを押すと、範囲の 4 隅にマーカーを置き、地図をその範囲へ寄せ、検索を 1 回だけ走らせ、「見え方」の 1 行を出します。押したあとも、既存のつまみ・地域の選択・地図操作はそのまま使えます。 + ## ライセンス MIT License です。[LICENSE](./LICENSE) を参照してください。 diff --git a/README.md b/README.md index fca7e25..39d486c 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,14 @@ in [CHANGELOG.md](./CHANGELOG.md). - Demo app: `demo/crs-candidate-lookup.html` - GitHub Pages target: `https://code4history.github.io/Atlas/` +The demo page has three preset contrasts. Each contrast has an A and a B button: + +- `place`: the same filters over Kyoto (A) and over Tokyo (B), so the candidate plane rectangular systems change (VI on one side, VIII and IX on the other). +- `historical-grid`: the Kyoto extent with the historical grid projections off (A) and on (B). B adds `UTJCP:ZONE-B` and exactly one row. +- `area-filter`: the Kyoto extent with the Japan area filter on (A) and off (B). Without the filter, CRSs from outside Japan join the list. + +Pressing a preset puts markers on the four corners of its extent, fits the map to it, runs one lookup, and writes a one-line description of what should be visible. The existing toggles, area select and map controls keep working afterwards. + ## License MIT License. See [LICENSE](./LICENSE). diff --git a/demo/crs-candidate-lookup.html b/demo/crs-candidate-lookup.html index b54861c..e2a2a03 100644 --- a/demo/crs-candidate-lookup.html +++ b/demo/crs-candidate-lookup.html @@ -45,6 +45,11 @@

Atlas - Candidate CRS Demo

初期化中... +
+
+

見え方:

+
+
操作: クリックでマーカー追加 - 全マーカーの bbox で CRS 候補検索。候補選択で CRS bbox を表示。
@@ -63,7 +68,11 @@

Atlas - Candidate CRS Demo

Candidate CRS

-

No query yet.

+
+

No query yet.

+

+
+

@@ -88,6 +97,12 @@

CRS Detail

+
+ + diff --git a/demo/main.d.ts b/demo/main.d.ts new file mode 100644 index 0000000..c9bd301 --- /dev/null +++ b/demo/main.d.ts @@ -0,0 +1,25 @@ +import type { ListProjectedCrsFromProjWasmOptions, ProjectedCrsInfo } from "../src/proj-wasm-crs.ts"; + +export interface DemoRuntime { + initializeProjWasmRuntime(options?: { projWasmAssetBaseUrl?: string | URL }): Promise | void; + listProjectedCrsFromProjWasm(options: ListProjectedCrsFromProjWasmOptions): Promise; +} + +export interface DemoState { + activePreset: string | null; + markers: [number, number][]; + rows: ProjectedCrsInfo[]; + runtimeReady: boolean; + areaFilterEnabled: boolean; + areaMode: string; + includeCustomProjections: boolean; + includeDeprecated: boolean; + onshoreOnly: boolean; +} + +export interface DemoHandle { + whenSettled(): Promise; + getState(): DemoState; +} + +export function initDemo(config?: { runtime?: DemoRuntime; doc?: unknown }): DemoHandle; diff --git a/demo/main.js b/demo/main.js index 757c87f..6b2da70 100644 --- a/demo/main.js +++ b/demo/main.js @@ -1,3 +1,4 @@ +import { version as packageVersion } from "../package.json"; import { TILE_SIZE, bboxScreenRects, @@ -8,6 +9,7 @@ import { worldToLonLat, } from "./map-geometry.ts"; import { wktText } from "./detail-text.ts"; +import { PRESETS, buildLookupOptions } from "./presets.js"; const MAP_WIDTH = 720; const MAP_HEIGHT = 460; @@ -23,7 +25,7 @@ const COUNTRIES = { china: { label: "China", area: "China", bounds: { west: 73, south: 18, east: 135, north: 54 }, center: [104, 35], zoom: 4 }, }; -const state = { +const DEFAULT_STATE = Object.freeze({ center: [...COUNTRIES.japan.center], zoom: COUNTRIES.japan.zoom, markers: [], @@ -38,12 +40,41 @@ const state = { includeDeprecated: false, onshoreOnly: false, includeCustomProjections: true, + excludedCount: 0, + outsideJapanCount: 0, + activePreset: null, +}); + +const state = { + ...DEFAULT_STATE, + center: [...DEFAULT_STATE.center], + markers: [...DEFAULT_STATE.markers], + rows: [...DEFAULT_STATE.rows], }; +let dom = globalThis.document; +let injectedRuntime = null; let atlasModulePromise; let dragState = null; +let settled = Promise.resolve(); + +function resetState() { + Object.assign(state, DEFAULT_STATE, { + center: [...DEFAULT_STATE.center], + markers: [], + rows: [], + }); +} + +/** Queue async work so tests can await everything a click started. */ +function enqueue(task) { + const next = settled.then(task); + settled = next.then(() => undefined, () => undefined); + return next; +} function loadAtlasRuntime() { + if (injectedRuntime) return Promise.resolve(injectedRuntime); if (!atlasModulePromise) { atlasModulePromise = import("@c4h/atlas"); } @@ -56,10 +87,17 @@ function projWasmAssetBaseUrl() { return new URL("./", import.meta.url); } - const pageRoot = new URL("../", document.baseURI); + const baseURI = dom?.baseURI && dom.baseURI !== "about:blank" ? dom.baseURI : "http://localhost/"; + const pageRoot = new URL("../", baseURI); return new URL("assets/", pageRoot); } +function atlasVersion() { + return typeof __ATLAS_VERSION__ === "string" && __ATLAS_VERSION__.length > 0 + ? __ATLAS_VERSION__ + : packageVersion; +} + function escapeHtml(value) { return String(value) .replaceAll("&", "&") @@ -92,9 +130,9 @@ function pointInBounds(point, bounds) { return point[0] >= bounds.west && point[0] <= bounds.east && point[1] >= bounds.south && point[1] <= bounds.north; } -function deriveAreaFilter() { - if (!state.areaFilterEnabled) return undefined; - if (state.areaMode !== "auto") return COUNTRIES[state.areaMode].area; +/** The area the `auto` mode derives from the first marker, if any. */ +function deriveAutoArea() { + if (state.areaMode !== "auto") return undefined; const firstMarker = state.markers[0]; if (!firstMarker) return undefined; return Object.entries(COUNTRIES) @@ -103,8 +141,19 @@ function deriveAreaFilter() { .find((country) => pointInBounds(firstMarker, country.bounds))?.area; } +function currentLookupInput() { + return { + extent: createExtent(state.markers), + areaFilterEnabled: state.areaFilterEnabled, + areaMode: state.areaMode, + includeCustomProjections: state.includeCustomProjections, + onshoreOnly: state.onshoreOnly, + includeDeprecated: state.includeDeprecated, + }; +} + function renderTiles() { - const layer = document.getElementById("tile-layer"); + const layer = dom.getElementById("tile-layer"); const center = centerWorld(); const topLeft = { x: center.x - MAP_WIDTH / 2, y: center.y - MAP_HEIGHT / 2 }; const minTileX = Math.floor(topLeft.x / TILE_SIZE); @@ -141,10 +190,13 @@ function drawRect(ctx, bbox, style) { } function renderMap() { - const canvas = document.getElementById("map-canvas"); - const ctx = canvas.getContext("2d"); - document.getElementById("map-label").textContent = `Slippy map - zoom ${state.zoom}`; + const canvas = dom.getElementById("map-canvas"); + dom.getElementById("map-label").textContent = `Slippy map - zoom ${state.zoom}`; renderTiles(); + + // jsdom has no 2D canvas; the rest of the page must still work without a context. + const ctx = canvas?.getContext?.("2d"); + if (!ctx) return; ctx.clearRect(0, 0, MAP_WIDTH, MAP_HEIGHT); if (state.markers.length) { @@ -189,7 +241,7 @@ function resultCaption() { } function renderTable() { - const body = document.getElementById("candidate-body"); + const body = dom.getElementById("candidate-body"); if (state.queryState === "idle") { body.innerHTML = ''; } else if (state.queryState === "loading") { @@ -215,11 +267,25 @@ function renderTable() { }; } - document.getElementById("result-count").textContent = resultCaption(); + dom.getElementById("result-count").textContent = resultCaption(); + + const summary = dom.getElementById("result-summary"); + if (summary) { + summary.textContent = state.queryState === "ready" + ? `地域名に Japan を含まない候補 ${state.outsideJapanCount} 件` + : ""; + } + + const excluded = dom.getElementById("excluded-summary"); + if (excluded) { + excluded.textContent = state.queryState === "ready" + ? `除外 ${state.excludedCount} 件(地球外・PROJ 文字列にできない行)` + : ""; + } } function renderDetail() { - const target = document.getElementById("detail-body"); + const target = dom.getElementById("detail-body"); const row = state.rows.find((item) => item.id === state.selectedId); if (state.queryState === "error") { @@ -251,15 +317,18 @@ function renderDetail() { } function renderStatus() { - document.getElementById("status").textContent = state.error ?? state.status; + dom.getElementById("status").textContent = state.error ?? state.status; } function renderInteractivity() { const disabled = !state.runtimeReady; - document.getElementById("map-board").classList.toggle("is-disabled", disabled); - for (const element of document.querySelectorAll("#area-enabled, #deprecated-enabled, #onshore-only, #custom-enabled, #area-mode, #clear-button")) { + dom.getElementById("map-board").classList.toggle("is-disabled", disabled); + for (const element of dom.querySelectorAll("#area-enabled, #deprecated-enabled, #onshore-only, #custom-enabled, #area-mode, #clear-button")) { element.disabled = disabled; } + for (const button of dom.querySelectorAll("#preset-bar button")) { + button.disabled = disabled; + } } function render() { @@ -274,6 +343,9 @@ async function runLookup() { state.selectedId = null; state.error = null; + state.excludedCount = 0; + state.outsideJapanCount = 0; + if (!state.markers.length) { state.rows = []; state.queryState = "idle"; @@ -282,22 +354,22 @@ async function runLookup() { return; } - const extent = createExtent(state.markers); - const area = deriveAreaFilter(); + const options = buildLookupOptions(currentLookupInput(), { autoArea: deriveAutoArea() }); + const area = options.area; state.queryState = "loading"; state.status = area ? `Querying with area filter: ${area}` : "Querying without area filter."; render(); try { const { listProjectedCrsFromProjWasm } = await loadAtlasRuntime(); + const excluded = []; state.rows = await listProjectedCrsFromProjWasm({ - extent, - area, - includeDeprecated: state.includeDeprecated, - onshoreOnly: state.onshoreOnly, - includeCustomProjections: state.includeCustomProjections, + ...options, projWasmAssetBaseUrl: projWasmAssetBaseUrl(), + onExcluded: (entries) => excluded.push(...entries), }); + state.excludedCount = excluded.length; + state.outsideJapanCount = state.rows.filter((row) => !String(row.areaName).includes("Japan")).length; state.queryState = "ready"; state.status = area ? `Queried with area filter: ${area}.` : "Queried without area filter."; } catch (error) { @@ -310,7 +382,7 @@ async function runLookup() { } function eventToCanvasPoint(event) { - const canvas = document.getElementById("map-canvas"); + const canvas = dom.getElementById("map-canvas"); const rect = canvas.getBoundingClientRect(); return { x: ((event.clientX - rect.left) / rect.width) * MAP_WIDTH, @@ -319,10 +391,10 @@ function eventToCanvasPoint(event) { } function bindMap() { - const canvas = document.getElementById("map-canvas"); + const canvas = dom.getElementById("map-canvas"); canvas.onpointerdown = (event) => { if (!state.runtimeReady) return; - canvas.setPointerCapture(event.pointerId); + canvas.setPointerCapture?.(event.pointerId); canvas.classList.add("dragging"); dragState = { pointerId: event.pointerId, @@ -348,16 +420,17 @@ function bindMap() { renderMap(); }; - canvas.onpointerup = async (event) => { + canvas.onpointerup = (event) => { if (!state.runtimeReady) return; if (!dragState || dragState.pointerId !== event.pointerId) return; - canvas.releasePointerCapture(event.pointerId); + canvas.releasePointerCapture?.(event.pointerId); canvas.classList.remove("dragging"); const wasClick = !dragState.moved; dragState = null; if (wasClick) { state.markers.push(screenToLonLat(eventToCanvasPoint(event), view())); - await runLookup(); + leavePreset(); + enqueue(runLookup); } }; @@ -380,38 +453,46 @@ function bindMap() { } function bindControls() { - document.getElementById("area-enabled").onchange = async (event) => { + dom.getElementById("area-enabled").onchange = (event) => { state.areaFilterEnabled = event.currentTarget.checked; - await runLookup(); + leavePreset(); + enqueue(runLookup); }; - document.getElementById("deprecated-enabled").onchange = async (event) => { + dom.getElementById("deprecated-enabled").onchange = (event) => { state.includeDeprecated = event.currentTarget.checked; - await runLookup(); + leavePreset(); + enqueue(runLookup); }; - document.getElementById("onshore-only").onchange = async (event) => { + dom.getElementById("onshore-only").onchange = (event) => { state.onshoreOnly = event.currentTarget.checked; - await runLookup(); + leavePreset(); + enqueue(runLookup); }; - document.getElementById("custom-enabled").onchange = async (event) => { + dom.getElementById("custom-enabled").onchange = (event) => { state.includeCustomProjections = event.currentTarget.checked; - await runLookup(); + leavePreset(); + enqueue(runLookup); }; - document.getElementById("area-mode").onchange = async (event) => { + dom.getElementById("area-mode").onchange = (event) => { state.areaMode = event.currentTarget.value; const country = COUNTRIES[state.areaMode]; if (country.center) { state.center = [...country.center]; state.zoom = country.zoom; } - await runLookup(); + leavePreset(); + enqueue(runLookup); }; - document.getElementById("clear-button").onclick = () => { + dom.getElementById("clear-button").onclick = () => { state.markers = []; state.rows = []; state.selectedId = null; state.queryState = "idle"; state.error = null; + state.excludedCount = 0; + state.outsideJapanCount = 0; state.status = "Cleared."; + leavePreset(); render(); }; } @@ -433,11 +514,150 @@ async function initializeRuntime() { render(); } -function boot() { +function cornersOf(extent) { + return [ + [extent.west, extent.south], + [extent.east, extent.south], + [extent.east, extent.north], + [extent.west, extent.north], + ]; +} + +/** A zoom at which the extent fits inside the map board with a little room around it. */ +function fitZoom(extent) { + const lonSpan = Math.max(extent.east - extent.west, 0.01); + const latSpan = Math.max(extent.north - extent.south, 0.01); + const midLatRad = (((extent.north + extent.south) / 2) * Math.PI) / 180; + const zoomForWidth = Math.log2((MAP_WIDTH * 0.8 * 360) / (TILE_SIZE * lonSpan)); + const zoomForHeight = Math.log2( + (MAP_HEIGHT * 0.8 * 360 * Math.cos(midLatRad)) / (TILE_SIZE * latSpan), + ); + return Math.max(2, Math.min(12, Math.floor(Math.min(zoomForWidth, zoomForHeight)))); +} + +function syncControls() { + const setChecked = (id, checked) => { + const element = dom.getElementById(id); + if (element) element.checked = checked; + }; + setChecked("area-enabled", state.areaFilterEnabled); + setChecked("deprecated-enabled", state.includeDeprecated); + setChecked("onshore-only", state.onshoreOnly); + setChecked("custom-enabled", state.includeCustomProjections); + const areaMode = dom.getElementById("area-mode"); + if (areaMode) areaMode.value = state.areaMode; +} + +function markPressed(activeId) { + for (const button of dom.querySelectorAll("#preset-bar button")) { + const pressed = button.dataset.preset === activeId; + button.setAttribute("aria-pressed", String(pressed)); + button.classList.toggle("active", pressed); + } +} + +function renderPresetBar() { + const bar = dom.getElementById("preset-bar"); + if (!bar) return; + while (bar.firstChild) bar.removeChild(bar.firstChild); + for (const preset of PRESETS) { + const group = dom.createElement("div"); + group.className = "atlas-preset-group"; + const title = dom.createElement("span"); + title.className = "atlas-preset-title"; + title.textContent = preset.title; + group.appendChild(title); + for (const option of preset.options) { + const button = dom.createElement("button"); + button.type = "button"; + button.className = "atlas-preset-btn"; + button.dataset.preset = `${preset.id}:${option.id}`; + button.textContent = option.label; + button.setAttribute("aria-pressed", "false"); + button.addEventListener("click", () => enqueue(() => applyPreset(preset, option))); + group.appendChild(button); + } + bar.appendChild(group); + } +} + +async function applyPreset(preset, option) { + const input = option.input; + state.activePreset = `${preset.id}:${option.id}`; + state.areaFilterEnabled = input.areaFilterEnabled; + state.areaMode = input.areaMode; + state.includeCustomProjections = input.includeCustomProjections; + state.onshoreOnly = input.onshoreOnly; + state.includeDeprecated = input.includeDeprecated; + state.markers = cornersOf(input.extent); + state.center = [ + (input.extent.west + input.extent.east) / 2, + (input.extent.south + input.extent.north) / 2, + ]; + state.zoom = fitZoom(input.extent); + state.selectedId = null; + syncControls(); + markPressed(state.activePreset); + const expectation = dom.getElementById("preset-expectation"); + if (expectation) expectation.textContent = option.expectation; + await runLookup(); +} + +/** + * Once a control or the map changes the input by hand, the pressed preset and its one-line + * description no longer describe what is on screen. + */ +function leavePreset() { + if (!state.activePreset) return; + state.activePreset = null; + markPressed(null); + const expectation = dom.getElementById("preset-expectation"); + if (expectation) { + expectation.textContent = + "手で変えた入力(プリセットではない)。プリセットのボタンを押すと戻る。"; + } +} + +function renderVersion() { + const target = dom.getElementById("version"); + if (!target) return; + target.textContent = `使っている版: @c4h/atlas ${atlasVersion()}`; +} + +/** + * Wire the demo page and return a small handle for tests. + * + * `runtime` is the same object shape as `@c4h/atlas` (`initializeProjWasmRuntime` and + * `listProjectedCrsFromProjWasm`); by default it is the dynamic import of the package. + * The page calls this once; importing this module does nothing by itself. + */ +export function initDemo({ runtime, doc } = {}) { + dom = doc ?? globalThis.document; + injectedRuntime = runtime ?? null; + atlasModulePromise = undefined; + dragState = null; + settled = Promise.resolve(); + resetState(); + + renderPresetBar(); + renderVersion(); bindMap(); bindControls(); render(); - void initializeRuntime(); -} + enqueue(initializeRuntime); -boot(); + return { + whenSettled: () => settled, + getState: () => ({ + activePreset: state.activePreset, + markers: state.markers.map((point) => [...point]), + rows: [...state.rows], + runtimeReady: state.runtimeReady, + areaFilterEnabled: state.areaFilterEnabled, + areaMode: state.areaMode, + includeCustomProjections: state.includeCustomProjections, + includeDeprecated: state.includeDeprecated, + onshoreOnly: state.onshoreOnly, + }), + }; +} diff --git a/demo/presets.d.ts b/demo/presets.d.ts new file mode 100644 index 0000000..faf4b40 --- /dev/null +++ b/demo/presets.d.ts @@ -0,0 +1,41 @@ +export interface PresetExtent { + west: number; + south: number; + east: number; + north: number; +} + +export interface PresetInput { + extent: PresetExtent; + areaFilterEnabled: boolean; + areaMode: string; + includeCustomProjections: boolean; + onshoreOnly: boolean; + includeDeprecated: boolean; +} + +export interface PresetOption { + id: string; + label: string; + expectation: string; + input: PresetInput; +} + +export interface PresetContrast { + id: string; + title: string; + options: PresetOption[]; +} + +export interface LookupOptions { + extent: PresetExtent; + area?: string; + includeDeprecated: boolean; + onshoreOnly: boolean; + includeCustomProjections: boolean; +} + +export const AREA_BY_MODE: Readonly>; +export const PRESETS: readonly PresetContrast[]; +export function findPreset(presetId: string, optionId: string): PresetOption | undefined; +export function buildLookupOptions(input: PresetInput, options?: { autoArea?: string }): LookupOptions; diff --git a/demo/presets.js b/demo/presets.js new file mode 100644 index 0000000..3071771 --- /dev/null +++ b/demo/presets.js @@ -0,0 +1,129 @@ +/** + * Preset values for the candidate CRS demo (data only — this module never touches the DOM). + * + * The demo page (`demo/main.js`) and the tests (`tests/demo-presets.test.ts`, + * `tests/demo-wiring.test.ts`) import these values, so the numbers and the expectation lines + * live here and nowhere else. A typo shows up as a test whose contrast no longer splits. + * + * `buildLookupOptions` is the single place that copies a preset input onto the arguments of + * `listProjectedCrsFromProjWasm`; the demo and the tests both use it so the copy cannot drift. + */ + +const KYOTO = Object.freeze({ west: 135.6, south: 34.9, east: 135.9, north: 35.1 }); +const TOKYO = Object.freeze({ west: 139.6, south: 35.6, east: 139.85, north: 35.75 }); + +/** Area names accepted by the proj.db `area` filter, keyed by the demo's select values. */ +export const AREA_BY_MODE = Object.freeze({ + japan: "Japan", + united_states: "United States", + korea: "Korea", + china: "China", +}); + +function presetInput(extent, { areaFilterEnabled, includeCustomProjections }) { + return Object.freeze({ + extent, + areaFilterEnabled, + areaMode: "japan", + includeCustomProjections, + onshoreOnly: false, + includeDeprecated: false, + }); +} + +export const PRESETS = Object.freeze([ + { + id: "place", + title: "場所による違い", + options: [ + { + id: "a", + label: "京都の範囲", + expectation: + "場所で候補の平面直角座標系が分かれる。A は VI 系(EPSG:6674)と歴史グリッド B 帯、B は VIII・IX 系(EPSG:6676・EPSG:6677)と A 帯。A に IX 系は無く、B に VI 系は無い", + input: presetInput(KYOTO, { areaFilterEnabled: true, includeCustomProjections: true }), + }, + { + id: "b", + label: "東京の範囲", + expectation: + "場所で候補の平面直角座標系が分かれる。A は VI 系(EPSG:6674)と歴史グリッド B 帯、B は VIII・IX 系(EPSG:6676・EPSG:6677)と A 帯。A に IX 系は無く、B に VI 系は無い", + input: presetInput(TOKYO, { areaFilterEnabled: true, includeCustomProjections: true }), + }, + ], + }, + { + id: "historical-grid", + title: "歴史グリッドの有無", + options: [ + { + id: "a", + label: "京都・歴史グリッドなし", + expectation: + "歴史グリッドの候補が出るか出ないかが分かれる。B にだけ UTJCP:ZONE-B が加わり、件数が 1 つ増える", + input: presetInput(KYOTO, { areaFilterEnabled: true, includeCustomProjections: false }), + }, + { + id: "b", + label: "京都・歴史グリッドあり", + expectation: + "歴史グリッドの候補が出るか出ないかが分かれる。B にだけ UTJCP:ZONE-B が加わり、件数が 1 つ増える", + input: presetInput(KYOTO, { areaFilterEnabled: true, includeCustomProjections: true }), + }, + ], + }, + { + id: "area-filter", + title: "地域で絞るかどうか", + options: [ + { + id: "a", + label: "京都・地域で絞る(Japan)", + expectation: + "地域で絞るかどうかで候補の広さが分かれる。B では日本以外の CRS も並び、件数が大きく増える。どちらでもエラーにならず、A に戻すと A の一覧に戻る", + input: presetInput(KYOTO, { areaFilterEnabled: true, includeCustomProjections: true }), + }, + { + id: "b", + label: "京都・地域で絞らない", + expectation: + "地域で絞るかどうかで候補の広さが分かれる。B では日本以外の CRS も並び、件数が大きく増える。どちらでもエラーにならず、A に戻すと A の一覧に戻る", + input: presetInput(KYOTO, { areaFilterEnabled: false, includeCustomProjections: true }), + }, + ], + }, +]); + +/** Find one preset option by contrast id and option id. */ +export function findPreset(presetId, optionId) { + return PRESETS.find((preset) => preset.id === presetId)?.options.find((option) => option.id === optionId); +} + +/** + * Copy one preset input (or the current control state) onto the lookup arguments. + * + * `areaFilterEnabled: false` leaves `area` out entirely rather than passing `undefined`, so the + * no-filter case is the same object shape the engine has always received. + * For `areaMode: 'auto'` the caller supplies the area it derived from the markers as `autoArea`. + */ +export function buildLookupOptions(input, { autoArea } = {}) { + const area = input.areaFilterEnabled + ? input.areaMode === "auto" + ? autoArea + : AREA_BY_MODE[input.areaMode] + : undefined; + + const options = { + extent: { + west: input.extent.west, + south: input.extent.south, + east: input.extent.east, + north: input.extent.north, + }, + includeDeprecated: input.includeDeprecated ?? false, + onshoreOnly: input.onshoreOnly ?? false, + includeCustomProjections: input.includeCustomProjections ?? false, + }; + if (area !== undefined) options.area = area; + return options; +} diff --git a/demo/style.css b/demo/style.css index 57ac315..201e425 100644 --- a/demo/style.css +++ b/demo/style.css @@ -154,3 +154,71 @@ tbody tr.selected { aspect-ratio: 36 / 23; } } + +.atlas-presets { + margin: 0 24px 8px; + padding: 10px 14px; + background: #f8f9fb; + border: 1px solid #eef0f4; + border-radius: 6px; +} + +.atlas-preset-bar { + display: flex; + flex-wrap: wrap; + gap: 16px; +} + +.atlas-preset-group { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; +} + +.atlas-preset-title { + font-size: 12px; + font-weight: 600; + color: #667; + margin-right: 2px; +} + +.atlas-preset-btn { + border: 1px solid #c9d2de; + background: #fff; + color: #1a2744; + border-radius: 4px; + padding: 5px 10px; + font-size: 12px; + cursor: pointer; +} + +.atlas-preset-btn[aria-pressed="true"] { + border-color: #3a7bd5; + background: #3a7bd5; + color: #fff; +} + +.atlas-preset-btn:disabled { + opacity: 0.5; + cursor: default; +} + +.atlas-expectation { + margin: 8px 0 0; + font-size: 13px; + color: #1a2744; +} + +.atlas-result-count-line { + display: flex; + align-items: baseline; + gap: 10px; + flex-wrap: wrap; +} + +.atlas-result-summary { + font-size: 12px; + color: #667; + margin: 0; +} diff --git a/package.json b/package.json index 82bd2eb..3b98e73 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ }, "devDependencies": { "@types/node": "^26.6.5", + "jsdom": "^30.0.0", "jszip": "^3.10.2", "typescript": "^6.0.3", "vite": "^7.3.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6bdf775..f77f8b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,9 @@ importers: '@types/node': specifier: ^26.6.5 version: 26.6.5 + jsdom: + specifier: ^30.0.0 + version: 30.1.3 jszip: specifier: ^3.10.2 version: 3.10.2 @@ -29,10 +32,58 @@ importers: version: 5.1.2(esbuild@0.28.2)(rollup@4.65.1)(typescript@6.0.3)(vite@7.3.7(@types/node@26.6.5)) vitest: specifier: ^5.0.4 - version: 5.0.5(@types/node@26.6.5)(vite@7.3.7(@types/node@26.6.5)) + version: 5.0.5(@types/node@26.6.5)(jsdom@30.1.3)(vite@7.3.7(@types/node@26.6.5)) packages: + '@asamuzakjp/css-color@7.1.9': + resolution: {integrity: sha512-vZ6JQVq8Ivgz6b/IBlAf90wZviXdluY13cQOPO+nJHbmXwGrPl2GcpqUY7a55/8ZVdfONXpCFMpRijOBCHNlcQ==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + + '@asamuzakjp/dom-selector@10.0.5': + resolution: {integrity: sha512-EgmJ7oEaGQECTn3H3wmnQpEy4hKY66VvM7VQndwPIQob3p877BgbyOUE3EX0LgXQCciF2H8KMdR+RznqjMNOLw==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + + '@bramus/specificity@2.4.3': + resolution: {integrity: sha512-0GEz8qJWokkUVYQYRukmkSHIFNoBH8QTreeHrAFbvz1XFYc3Y+puhMs8PQ5x8GNugiC2l5jvDBXKLbBIByeSBw==} + hasBin: true + + '@csstools/color-helpers@6.1.2': + resolution: {integrity: sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.4.3': + resolution: {integrity: sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.2 + '@csstools/css-tokenizer': ^4.0.2 + + '@csstools/css-color-parser@4.2.6': + resolution: {integrity: sha512-iiPQ3iRWwnJkeEn6RIu6SJPr7hYrLz6XZ9s/QZl+2/LI5KQVjpl2fdmDSZKuD4xP6GMmMPgHFFXg6k1Wkz0Trg==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.2 + '@csstools/css-tokenizer': ^4.0.2 + + '@csstools/css-parser-algorithms@4.0.2': + resolution: {integrity: sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.2 + + '@csstools/css-syntax-patches-for-csstree@1.1.15': + resolution: {integrity: sha512-J0u7HkVl2nzSlhsiTOp4AmwcUQ3D+mGEEKfBy/7To5/y7F2OHwyLrXfrhR0SMgr4p5Lo+eaMVSeai24zUcBIxA==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.2': + resolution: {integrity: sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==} + engines: {node: '>=20.19.0'} + '@esbuild/aix-ppc64@0.28.2': resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} engines: {node: '>=18'} @@ -189,6 +240,15 @@ packages: cpu: [x64] os: [win32] + '@exodus/bytes@1.16.0': + resolution: {integrity: sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -420,6 +480,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + bidi-js@1.1.0: + resolution: {integrity: sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -460,6 +523,14 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + data-urls@8.0.0: + resolution: {integrity: sha512-JQUCCK+/CAGbGGx618PlFZMXH81Jtj7t73CHv9dd/zVWvxgGT2zLFRjnKb+Ng46GIwcy+f7f88qd4duqllK+WQ==} + engines: {node: ^22.14.0 || >=24.0.0} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -469,6 +540,13 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + entities@8.1.0: + resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==} + engines: {node: '>=20.19.0'} + es-module-lexer@2.3.2: resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} @@ -515,6 +593,10 @@ packages: glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + html-encoding-sniffer@7.0.0: + resolution: {integrity: sha512-UikN5yr7xsCDAq87Or5or0PAlD3HJJOKVzM05az588WnpDJ4Ux7a2A53Qi6gofGg2/EtvF/H4hCi/TXfCW4Y6w==} + engines: {node: ^22.13.0 || >=24.0.0} + immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} @@ -537,9 +619,21 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + jsdom@30.1.3: + resolution: {integrity: sha512-hQMUPvRgN7KxsfIytDQji7bimuJeRQa6Qo5+iuiZQk5CafeRxLIA10/cG5YCBFNpD9RO3oqIiN4CO1Y44kxqXg==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + peerDependencies: + canvas: ^3.2.3 + peerDependenciesMeta: + canvas: + optional: true + jszip@3.10.2: resolution: {integrity: sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==} @@ -553,12 +647,19 @@ packages: resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==} engines: {node: '>=14'} + lru-cache@11.5.3: + resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} + engines: {node: 20 || >=22} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} magic-string@1.4.4: resolution: {integrity: sha512-jQz9sVhrZuphH4NrAIHSgkDJCluQO7Ioi4KGRLa7aTUjAODQ13hENX0YIytqYE/rIkO485gQG2LeBxJaW53kkQ==} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} @@ -581,6 +682,9 @@ packages: pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -614,6 +718,10 @@ packages: proj-wasm@0.1.0-alpha8: resolution: {integrity: sha512-vMWnEz4I6Lx5ruwFr2c2XpRdDFZTNI/rnvSNS8SUroWhdi5VyHruXXzb04XoZ8vvl6jJbl4YUAPz0YMLX1di0Q==} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -628,6 +736,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resource-tracker@0.0.1-alpha1: resolution: {integrity: sha512-MHZmmHNZtYy42gP1/JJNwmg1JoUYKZuh8TVojIXRziljglnKQi2zLLeP8bmI6m37Fqkhz/on9ph8qHi/YdsTTQ==} @@ -639,6 +751,10 @@ packages: safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + setimmediate@1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} @@ -672,10 +788,25 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tldts-core@7.4.19: + resolution: {integrity: sha512-goNXV6Ho9TIOfpJZanTd6CKG5DyYgYAL+k+22T0SLh8l6+KHIfBUPFuePuSZv05UttIaaLmI9tb7TmGIdK5AkQ==} + + tldts@7.4.19: + resolution: {integrity: sha512-48dH6aSpQnpbzDUc4vIQZ1VjB8tkK+e8c1yx05/SZK+rkssgjEOlxDd/yU8/a4vGYQ9vRgJr9dwMq9TJ0b3X2Q==} + hasBin: true + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + + tr46@7.0.0: + resolution: {integrity: sha512-BcDSDW+SIGWVOns1Zm6Yg91q9AEj7eaID0QDY9oIqJD1A8dscNwMcl9Sms2DeuWCNov1X0/oXanHGYQBSLwZsg==} + engines: {node: ^22.14.0 || >=24.0.0} + typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} @@ -687,6 +818,10 @@ packages: undici-types@8.9.0: resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} + undici@8.12.3: + resolution: {integrity: sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw==} + engines: {node: '>=22.19.0'} + unplugin-dts@1.1.2: resolution: {integrity: sha512-RDqy90XUmFj6rz4U5YoTMdYdaCJe2ZnQZaWIjkWS4952Gv0zBTQyqWtlGgVMPYHa982dURvyoc+sUOFIcDFv+w==} peerDependencies: @@ -835,9 +970,25 @@ packages: vscode-uri@3.2.0: resolution: {integrity: sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==} + w3c-xmlserializer@6.0.0: + resolution: {integrity: sha512-4Nsy8K5Tr6SPDH9jhKJOHf7ChDrc1zufZTVSF7x72hwuEXBqxqk9G6cK+K2NRUtB3iELRJqjXb4JPDMBjMTl2Q==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@17.3.0: + resolution: {integrity: sha512-KwOAJrpf+GTgBvcBQJcP38sMF0DsQKdwtG+TxM2pW44P6E5k0hDbWrDmyuJDIF0Uef0aQq0ZoJs+5oI+k8ddhg==} + engines: {node: ^22.14.0 || >=24.0.0} + why-is-node-running@3.2.1: resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==} engines: {node: '>=20.11'} @@ -847,8 +998,58 @@ packages: resolution: {integrity: sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==} engines: {node: '>= 6'} + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + snapshots: + '@asamuzakjp/css-color@7.1.9': + dependencies: + '@csstools/css-calc': 3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-color-parser': 4.2.6(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 + lru-cache: 11.5.3 + + '@asamuzakjp/dom-selector@10.0.5': + dependencies: + bidi-js: 1.1.0 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.3 + + '@bramus/specificity@2.4.3': + dependencies: + css-tree: 3.2.1 + + '@csstools/color-helpers@6.1.2': {} + + '@csstools/css-calc@3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 + + '@csstools/css-color-parser@4.2.6(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': + dependencies: + '@csstools/color-helpers': 6.1.2 + '@csstools/css-calc': 3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 + + '@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2)': + dependencies: + '@csstools/css-tokenizer': 4.0.2 + + '@csstools/css-syntax-patches-for-csstree@1.1.15(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.2': {} + '@esbuild/aix-ppc64@0.28.2': optional: true @@ -927,6 +1128,8 @@ snapshots: '@esbuild/win32-x64@0.28.2': optional: true + '@exodus/bytes@1.16.0': {} + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.6.0 @@ -1089,6 +1292,10 @@ snapshots: assertion-error@2.0.1: {} + bidi-js@1.1.0: + dependencies: + require-from-string: 2.0.2 + binary-extensions@2.3.0: {} braces@3.0.3: @@ -1129,10 +1336,26 @@ snapshots: core-util-is@1.0.3: {} + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.2 + + data-urls@8.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 17.3.0 + transitivePeerDependencies: + - '@noble/hashes' + debug@4.4.3: dependencies: ms: 2.1.3 + decimal.js@10.6.0: {} + + entities@8.1.0: {} + es-module-lexer@2.3.2: {} esbuild@0.28.2: @@ -1191,6 +1414,12 @@ snapshots: glob-to-regexp@0.4.1: {} + html-encoding-sniffer@7.0.0: + dependencies: + '@exodus/bytes': 1.16.0 + transitivePeerDependencies: + - '@noble/hashes' + immediate@3.0.6: {} inherits@2.0.4: {} @@ -1207,8 +1436,35 @@ snapshots: is-number@7.0.0: {} + is-potential-custom-element-name@1.0.1: {} + isarray@1.0.0: {} + jsdom@30.1.3: + dependencies: + '@asamuzakjp/css-color': 7.1.9 + '@asamuzakjp/dom-selector': 10.0.5 + '@bramus/specificity': 2.4.3 + '@csstools/css-syntax-patches-for-csstree': 1.1.15(css-tree@3.2.1) + '@exodus/bytes': 1.16.0 + css-tree: 3.2.1 + data-urls: 8.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 7.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.3 + parse5: 8.0.1 + saxes: 6.0.0 + tough-cookie: 6.0.2 + undici: 8.12.3 + w3c-xmlserializer: 6.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 17.3.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + jszip@3.10.2: dependencies: lie: 3.3.0 @@ -1228,6 +1484,8 @@ snapshots: pkg-types: 2.4.0 quansync: 0.2.11 + lru-cache@11.5.3: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 @@ -1236,6 +1494,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 + mdn-data@2.27.1: {} + mlly@1.8.2: dependencies: acorn: 8.19.0 @@ -1253,6 +1513,10 @@ snapshots: pako@1.0.11: {} + parse5@8.0.1: + dependencies: + entities: 8.1.0 + path-browserify@1.0.1: {} pathe@2.0.3: {} @@ -1289,6 +1553,8 @@ snapshots: resource-tracker: 0.0.1-alpha1 xhr2: 0.2.1 + punycode@2.3.1: {} + quansync@0.2.11: {} readable-stream@2.3.8: @@ -1307,6 +1573,8 @@ snapshots: readdirp@4.1.2: {} + require-from-string@2.0.2: {} + resource-tracker@0.0.1-alpha1: dependencies: cherry-cljs: 0.3.23 @@ -1345,6 +1613,10 @@ snapshots: safe-buffer@5.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + setimmediate@1.0.5: {} source-map-js@1.2.2: {} @@ -1372,16 +1644,32 @@ snapshots: fdir: 6.5.0(picomatch@4.0.7) picomatch: 4.0.7 + tldts-core@7.4.19: {} + + tldts@7.4.19: + dependencies: + tldts-core: 7.4.19 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.19 + + tr46@7.0.0: + dependencies: + punycode: 2.3.1 + typescript@6.0.3: {} ufo@1.6.4: {} undici-types@8.9.0: {} + undici@8.12.3: {} + unplugin-dts@1.1.2(esbuild@0.28.2)(rollup@4.65.1)(typescript@6.0.3)(vite@7.3.7(@types/node@26.6.5)): dependencies: '@rollup/pluginutils': 5.5.1(rollup@4.65.1) @@ -1437,7 +1725,7 @@ snapshots: '@types/node': 26.6.5 fsevents: 2.3.3 - vitest@5.0.5(@types/node@26.6.5)(vite@7.3.7(@types/node@26.6.5)): + vitest@5.0.5(@types/node@26.6.5)(jsdom@30.1.3)(vite@7.3.7(@types/node@26.6.5)): dependencies: '@types/chai': 5.2.3 '@vitest/mocker': 5.0.5(vite@7.3.7(@types/node@26.6.5)) @@ -1455,6 +1743,7 @@ snapshots: why-is-node-running: 3.2.1 optionalDependencies: '@types/node': 26.6.5 + jsdom: 30.1.3 transitivePeerDependencies: - msw @@ -1471,8 +1760,28 @@ snapshots: vscode-uri@3.2.0: {} + w3c-xmlserializer@6.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.1: {} + webpack-virtual-modules@0.6.2: {} + whatwg-mimetype@5.0.0: {} + + whatwg-url@17.3.0: + dependencies: + '@exodus/bytes': 1.16.0 + tr46: 7.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + why-is-node-running@3.2.1: {} xhr2@0.2.1: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} diff --git a/tests/demo-presets.test.ts b/tests/demo-presets.test.ts new file mode 100644 index 0000000..7ee96d2 --- /dev/null +++ b/tests/demo-presets.test.ts @@ -0,0 +1,149 @@ +/** + * Demo presets: every contrast in `demo/presets.js` must split with the real lookup engine. + * + * This file calls `listProjectedCrsFromProjWasm` (from `src`, not `dist`) with the exact inputs + * the demo page uses and reads the arguments through the same `buildLookupOptions` helper the + * page uses, so a wrong number in the presets or a wrong copy in the page shows up here. + * + * Counts are logged, not baked: the assertions check which systems the contrast promises, + * not the exact size of the catalog. + */ +import { describe, expect, it } from "vitest"; +import { + listProjectedCrsFromProjWasm, + type ListProjectedCrsFromProjWasmOptions, + type ProjectedCrsInfo, +} from "../src/proj-wasm-crs.ts"; +import { buildLookupOptions, findPreset } from "../demo/presets.js"; + +const CALL_TIMEOUT_MS = 60_000; + +async function listWithTimeout( + options: ListProjectedCrsFromProjWasmOptions, +): Promise { + let timer: ReturnType | undefined; + try { + return await Promise.race([ + listProjectedCrsFromProjWasm(options), + new Promise((_, reject) => { + timer = setTimeout( + () => reject(new Error(`listProjectedCrsFromProjWasm did not answer within ${CALL_TIMEOUT_MS} ms`)), + CALL_TIMEOUT_MS, + ); + }), + ]); + } finally { + clearTimeout(timer); + } +} + +function presetOptions(presetId: string, optionId: string): ListProjectedCrsFromProjWasmOptions { + const preset = findPreset(presetId, optionId); + if (preset === undefined) throw new Error(`preset ${presetId}:${optionId} is missing`); + return buildLookupOptions(preset.input); +} + +function ids(rows: ProjectedCrsInfo[]): string[] { + return rows.map((row) => row.id); +} + +function hasAreaNameWithoutJapan(rows: ProjectedCrsInfo[]): boolean { + return rows.some((row) => !row.areaName.includes("Japan")); +} + +describe("demo presets: place", () => { + it( + "moves the plane rectangular systems between Kyoto and Tokyo", + async () => { + const a = await listWithTimeout(presetOptions("place", "a")); + const b = await listWithTimeout(presetOptions("place", "b")); + console.info(`[demo-presets] place: A ${a.length} rows, B ${b.length} rows`); + + expect(ids(a)).toContain("EPSG:6674"); + expect(ids(a)).not.toContain("EPSG:6677"); + expect(ids(b)).toContain("EPSG:6677"); + expect(ids(b)).toContain("EPSG:6676"); + expect(ids(b)).not.toContain("EPSG:6674"); + }, + CALL_TIMEOUT_MS * 2 + 60_000, + ); +}); + +describe("demo presets: historical-grid", () => { + it( + "adds only the historical grid zone B and exactly one row", + async () => { + const without = await listWithTimeout(presetOptions("historical-grid", "a")); + const withGrid = await listWithTimeout(presetOptions("historical-grid", "b")); + console.info( + `[demo-presets] historical-grid: A ${without.length} rows, B ${withGrid.length} rows`, + ); + + expect(ids(withGrid)).toContain("UTJCP:ZONE-B"); + expect(ids(without)).not.toContain("UTJCP:ZONE-B"); + expect(withGrid.length).toBe(without.length + 1); + }, + CALL_TIMEOUT_MS * 2 + 60_000, + ); +}); + +describe("demo presets: area-filter", () => { + it( + "survives the no-filter sequence and returns the same A list after B and back", + async () => { + const a = presetOptions("area-filter", "a"); + const b = presetOptions("area-filter", "b"); + const bWithoutCustomProjections = buildLookupOptions({ + ...findPreset("area-filter", "b")!.input, + includeCustomProjections: false, + }); + + const excludedByCall: number[] = []; + const run = async (options: ListProjectedCrsFromProjWasmOptions) => { + let excluded = 0; + const rows = await listWithTimeout({ + ...options, + onExcluded: (entries) => { + excluded += entries.length; + }, + }); + excludedByCall.push(excluded); + return rows; + }; + + const noArea = await run(bWithoutCustomProjections); + const noAreaCustom = await run(b); + const japanCustom = await run(a); + const noAreaAgain = await run(b); + + console.info( + `[demo-presets] area-filter: no area ${noArea.length} rows, no area + custom ${noAreaCustom.length} rows, ` + + `Japan + custom ${japanCustom.length} rows, no area again ${noAreaAgain.length} rows; ` + + `excluded ${excludedByCall.join(", ")}`, + ); + + // The third call is the one that used to fail after the no-filter calls. + expect(ids(japanCustom)).toContain("EPSG:6674"); + expect(ids(japanCustom)).toContain("UTJCP:ZONE-B"); + + // A round trip through the presets must not error and must come back to the same list. + const roundTripA = await run(a); + const roundTripB = await run(b); + const roundTripAAgain = await run(a); + + console.info( + `[demo-presets] area-filter round trip: A ${roundTripA.length} rows, B ${roundTripB.length} rows, ` + + `A again ${roundTripAAgain.length} rows`, + ); + + for (const rows of [noArea, noAreaCustom, japanCustom, noAreaAgain, roundTripA, roundTripB, roundTripAAgain]) { + expect(rows.length).toBeGreaterThan(0); + } + expect(roundTripB.length).toBeGreaterThan(roundTripA.length); + expect(hasAreaNameWithoutJapan(roundTripB)).toBe(true); + expect(ids(roundTripAAgain)).toEqual(ids(roundTripA)); + expect(ids(japanCustom)).toEqual(ids(roundTripA)); + }, + CALL_TIMEOUT_MS * 7 + 60_000, + ); +}); diff --git a/tests/demo-wiring.test.ts b/tests/demo-wiring.test.ts new file mode 100644 index 0000000..7110318 --- /dev/null +++ b/tests/demo-wiring.test.ts @@ -0,0 +1,229 @@ +// @vitest-environment jsdom +/// +/** + * Demo page wiring (jsdom, injected stub runtime). + * + * The DOM is built by reading `demo/crs-candidate-lookup.html` itself, so renamed ids in the + * page break this test. The lookup runtime is injected, so no proj-wasm work happens here; + * the real engine is covered by `tests/demo-presets.test.ts`. + * + * jsdom has no 2D canvas (`getContext` returns `null`), so the page must keep working without + * a drawing context. Only `getBoundingClientRect` is given a size, because jsdom has no layout. + */ +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { initDemo } from "../demo/main.js"; +import { PRESETS, buildLookupOptions } from "../demo/presets.js"; +import { version as packageVersion } from "../package.json"; + +const testsDir = dirname(fileURLToPath(import.meta.url)); +const pageHtml = readFileSync(join(testsDir, "..", "demo", "crs-candidate-lookup.html"), "utf8"); +const bodyInner = /]*>([\s\S]*)<\/body>/i.exec(pageHtml)?.[1]; + +const MAP = { width: 720, height: 460 }; + +function mountPage(): void { + if (bodyInner === undefined) throw new Error("cannot read of crs-candidate-lookup.html"); + document.body.innerHTML = bodyInner.replace(//gi, ""); + const canvas = document.getElementById("map-canvas") as HTMLCanvasElement; + canvas.getBoundingClientRect = () => + ({ left: 0, top: 0, right: MAP.width, bottom: MAP.height, ...MAP, x: 0, y: 0, toJSON: () => ({}) }) as DOMRect; + canvas.setPointerCapture = () => {}; + canvas.releasePointerCapture = () => {}; +} + +function fakeRow(overrides: Record = {}) { + return { + id: "EPSG:6674", + authority: "EPSG", + code: "6674", + name: "JGD2011 / Japan Plane Rectangular CS VI", + areaName: "Japan - onshore", + deprecated: false, + bbox: { west: 134, south: 33, east: 138, north: 37 }, + wkt2: null, + proj4: "+proj=tmerc +lat_0=36 +lon_0=136 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +units=m +no_defs", + ...overrides, + }; +} + +function makeRuntime(rows: ReturnType[] = [fakeRow()]) { + const listProjectedCrsFromProjWasm = vi.fn(async (_options: unknown) => rows); + return { + listProjectedCrsFromProjWasm, + runtime: { + initializeProjWasmRuntime: vi.fn(async () => undefined), + listProjectedCrsFromProjWasm, + }, + }; +} + +function text(id: string): string { + return document.getElementById(id)?.textContent ?? ""; +} + +function pressedButtons(): string[] { + return [...document.querySelectorAll("#preset-bar button[aria-pressed='true']")].map( + (button) => button.dataset.preset ?? "", + ); +} + +function clickPreset(preset: string, option: string): void { + const button = document.querySelector(`[data-preset="${preset}:${option}"]`); + if (!button) throw new Error(`preset button ${preset}:${option} is missing`); + button.click(); +} + +async function start(runtime: ReturnType["runtime"]) { + const demo = initDemo({ runtime, doc: document }); + await demo.whenSettled(); + return demo; +} + +const allOptions = PRESETS.flatMap((preset) => + preset.options.map((option) => ({ presetId: preset.id, optionId: option.id, option })), +); + +describe("demo wiring: preset buttons", () => { + beforeEach(() => { + mountPage(); + }); + + it("renders one button per preset option with the labels from presets.js", async () => { + const { runtime } = makeRuntime(); + await start(runtime); + const buttons = [...document.querySelectorAll("#preset-bar button")]; + expect(buttons.map((button) => button.dataset.preset)).toEqual( + allOptions.map(({ presetId, optionId }) => `${presetId}:${optionId}`), + ); + expect(buttons.map((button) => button.textContent)).toEqual(allOptions.map(({ option }) => option.label)); + expect(document.querySelectorAll("#preset-bar button[aria-pressed='true']")).toHaveLength(0); + }); + + for (const { presetId, optionId, option } of allOptions) { + it(`button ${presetId}:${optionId} calls the lookup once with the preset input and shows its line`, async () => { + const { runtime, listProjectedCrsFromProjWasm } = makeRuntime(); + const demo = await start(runtime); + expect(listProjectedCrsFromProjWasm).not.toHaveBeenCalled(); + + clickPreset(presetId, optionId); + await demo.whenSettled(); + + expect(listProjectedCrsFromProjWasm).toHaveBeenCalledTimes(1); + const options = listProjectedCrsFromProjWasm.mock.calls[0][0] as ReturnType; + const expected = buildLookupOptions(option.input); + expect(options.extent).toEqual(expected.extent); + expect(options.area).toBe(expected.area); + expect(options.includeCustomProjections).toBe(expected.includeCustomProjections); + expect(options.includeDeprecated).toBe(false); + expect(options.onshoreOnly).toBe(false); + + expect(pressedButtons()).toEqual([`${presetId}:${optionId}`]); + expect(text("preset-expectation")).toBe(option.expectation); + }); + } + + it("goes back to area: 'Japan' after pressing the no-filter button", async () => { + const { runtime, listProjectedCrsFromProjWasm } = makeRuntime(); + const demo = await start(runtime); + + clickPreset("area-filter", "b"); + await demo.whenSettled(); + const withoutArea = listProjectedCrsFromProjWasm.mock.calls[0][0] as { area?: string }; + expect(withoutArea.area).toBeUndefined(); + + clickPreset("area-filter", "a"); + await demo.whenSettled(); + expect(listProjectedCrsFromProjWasm).toHaveBeenCalledTimes(2); + const withArea = listProjectedCrsFromProjWasm.mock.calls[1][0] as { area?: string }; + expect(withArea.area).toBe("Japan"); + expect(pressedButtons()).toEqual(["area-filter:a"]); + }); +}); + +describe("demo wiring: defaults and footer", () => { + beforeEach(() => { + mountPage(); + }); + + it("keeps the opening defaults (Japan, custom on, onshore off) on the first lookup", async () => { + const { runtime, listProjectedCrsFromProjWasm } = makeRuntime(); + const demo = await start(runtime); + expect(listProjectedCrsFromProjWasm).not.toHaveBeenCalled(); + + const canvas = document.getElementById("map-canvas") as HTMLCanvasElement; + canvas.dispatchEvent(new MouseEvent("pointerdown", { clientX: 360, clientY: 230, bubbles: true })); + canvas.dispatchEvent(new MouseEvent("pointerup", { clientX: 360, clientY: 230, bubbles: true })); + await demo.whenSettled(); + + expect(listProjectedCrsFromProjWasm).toHaveBeenCalledTimes(1); + const options = listProjectedCrsFromProjWasm.mock.calls[0][0] as { + area?: string; + includeCustomProjections?: boolean; + includeDeprecated?: boolean; + onshoreOnly?: boolean; + }; + expect(options.area).toBe("Japan"); + expect(options.includeCustomProjections).toBe(true); + expect(options.includeDeprecated).toBe(false); + expect(options.onshoreOnly).toBe(false); + + const state = demo.getState(); + expect(state.areaMode).toBe("japan"); + expect(state.areaFilterEnabled).toBe(true); + expect(state.includeCustomProjections).toBe(true); + expect(state.onshoreOnly).toBe(false); + }); + + it("shows the package version in the footer", async () => { + const { runtime } = makeRuntime(); + await start(runtime); + expect(text("version")).toContain(`@c4h/atlas ${packageVersion}`); + }); + + it("shows how many candidates are outside Japan and how many rows were excluded", async () => { + const rows = [ + fakeRow(), + fakeRow({ id: "EPSG:32633", name: "WGS 84 / UTM 33N", areaName: "World" }), + fakeRow({ id: "EPSG:3857", name: "Web Mercator", areaName: "World" }), + ]; + const { runtime } = makeRuntime(rows); + const demo = await start(runtime); + + clickPreset("area-filter", "a"); + await demo.whenSettled(); + + expect(text("result-summary")).toBe("地域名に Japan を含まない候補 2 件"); + expect(text("excluded-summary")).toContain("0"); + }); + + it("shows the excluded count reported by onExcluded", async () => { + const listProjectedCrsFromProjWasm = vi.fn( + async (options: { onExcluded?: (entries: { id: string; name: string; reason: string }[]) => void }) => { + options.onExcluded?.([ + { id: "EPSG:32600", name: "Mars", reason: "Not on the Earth" }, + { id: "EPSG:9999", name: "Broken method", reason: "Unsupported conversion method" }, + ]); + return [fakeRow()]; + }, + ); + const demo = initDemo({ + runtime: { + initializeProjWasmRuntime: vi.fn(async () => undefined), + listProjectedCrsFromProjWasm, + }, + doc: document, + }); + await demo.whenSettled(); + + clickPreset("area-filter", "a"); + await demo.whenSettled(); + + expect(text("excluded-summary")).toContain("2"); + expect(text("excluded-summary")).toContain("地球外"); + }); +}); diff --git a/vite.config.ts b/vite.config.ts index d43a6fe..a7d7c22 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,7 @@ import { defineConfig } from "vitest/config"; import dts from "vite-plugin-dts"; import { copyFile, mkdir, readFile } from "node:fs/promises"; +import { readFileSync } from "node:fs"; import { createRequire } from "node:module"; import { basename, dirname, extname, join, resolve } from "node:path"; import process from "node:process"; @@ -10,6 +11,7 @@ const isPagesBuild = process.env.GITHUB_PAGES === "true"; const require = createRequire(import.meta.url); const projWasmDist = dirname(require.resolve("proj-wasm")); const customProjectionJson = resolve(__dirname, "data/crs/custom-projections.json"); +const packageVersion = (JSON.parse(readFileSync(resolve(__dirname, "package.json"), "utf8")) as { version: string }).version; export const PROJ_WASM_RUNTIME_ASSETS = [ "proj.db", @@ -133,6 +135,7 @@ export const shouldExternalize = (id: string) => { }; export default defineConfig({ + define: isPackageBuild ? {} : { __ATLAS_VERSION__: JSON.stringify(packageVersion) }, base: isPackageBuild ? undefined : isPagesBuild ? "/Atlas/" : "/", build: isPackageBuild ? { @@ -183,5 +186,8 @@ export default defineConfig({ }, test: { environment: "node", + // proj-wasm starts a worker pool per test file; running files at the same time makes the + // engine tests compete for memory and hit their timeouts at random. + fileParallelism: false, }, });
Click the map to start a query.