diff --git a/data/core.yaml b/data/core.yaml
index 937f885ca..16b52890f 100644
--- a/data/core.yaml
+++ b/data/core.yaml
@@ -25,7 +25,9 @@ en:
zoom_overview_label: Zoom 14
zoom_overview_tooltip: Snap to zoom 14 so tiles stop loading and panning stays fast.
Buildings render from z16.
plateau_conflation:
- osm_layer_off: "Plateau buildings stay hidden while the OpenStreetMap data layer is off, because existing buildings cannot be checked for duplicates. Press Shift+O to turn it back on."
+ osm_layer_off: "PLATEAU buildings stay hidden while the OpenStreetMap data layer is off, because existing buildings cannot be checked for duplicates. Press Shift+O to turn it back on."
+ osm_layer_off_title: PLATEAU buildings are hidden
+ dont_show_again: Do not show this again
height_transfer:
section_title: Plateau tags
additions: Add from Plateau
diff --git a/data/l10n/core.en.json b/data/l10n/core.en.json
index 1ab7ea448..2b5c31b0e 100644
--- a/data/l10n/core.en.json
+++ b/data/l10n/core.en.json
@@ -28,7 +28,9 @@
"zoom_overview_tooltip": "Snap to zoom 14 so tiles stop loading and panning stays fast.
Buildings render from z16."
},
"plateau_conflation": {
- "osm_layer_off": "Plateau buildings stay hidden while the OpenStreetMap data layer is off, because existing buildings cannot be checked for duplicates. Press Shift+O to turn it back on."
+ "osm_layer_off": "PLATEAU buildings stay hidden while the OpenStreetMap data layer is off, because existing buildings cannot be checked for duplicates. Press Shift+O to turn it back on.",
+ "osm_layer_off_title": "PLATEAU buildings are hidden",
+ "dont_show_again": "Do not show this again"
},
"height_transfer": {
"section_title": "Plateau tags",
diff --git a/data/l10n/core.ja.json b/data/l10n/core.ja.json
index 7da163cab..fdd4a706e 100644
--- a/data/l10n/core.ja.json
+++ b/data/l10n/core.ja.json
@@ -2963,7 +2963,9 @@
"hires": "高解像度"
},
"plateau_conflation": {
- "osm_layer_off": "OpenStreetMap のデータのレイヤーが消えているあいだは、Plateau の建物を表示しません。既存の建物と重なるかを確かめられないためです。Shift+O で戻せます。"
+ "osm_layer_off": "OpenStreetMap のデータのレイヤーが消えているあいだは、PLATEAU の建物を表示しません。既存の建物と重なるかを確かめられないためです。Shift+O で戻せます。",
+ "osm_layer_off_title": "PLATEAU の建物を表示していません",
+ "dont_show_again": "次から表示しない"
},
"preferences": {
"color_selection": {
diff --git a/docs/superpowers/plans/2026-09-08-plateau-osm-layer-off-dialog.md b/docs/superpowers/plans/2026-09-08-plateau-osm-layer-off-dialog.md
new file mode 100644
index 000000000..caa1bfcca
--- /dev/null
+++ b/docs/superpowers/plans/2026-09-08-plateau-osm-layer-off-dialog.md
@@ -0,0 +1,610 @@
+# OSM のレイヤー消灯をダイアログで伝える実装計画
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**目標:** OSM のデータのレイヤーが消えているために PLATEAU の候補を伏せたことを、画面下端の一時的な通知ではなく、ページを開き直すごとに 1 回だけ出るダイアログで伝えます。
+
+**構成:** `PlateauService` は `osmlayeroff` という出来事を 1 回だけ発生させるだけにし、画面に何を出すかは新しい部品 `UiPlateauOsmLayerOffDialog` が決めます。ダイアログは既存の `uiConfirm` で組み立て、「次から表示しない」の状態を `StorageSystem` に保存します。
+
+**設計文書:** `docs/superpowers/specs/2026-09-08-plateau-osm-layer-off-dialog-design.ja.md`
+
+**使う道具:** JavaScript (ES modules)、d3-selection、karma と mocha と chai、happen
+
+## 全体の制約
+
+- リポジトリは `/Users/nyampire/git/Rapid`、ブランチは `feature/plateau-osm-layer-off-notice`、起点は `fb97052fc` です。
+- 保存する名前は `plateau.osm-layer-off-dialog.hidden` です。値は文字列の `'true'` だけを使います。
+- 出来事の名前は `osmlayeroff` です。
+- 文言の名前は `plateau_conflation.osm_layer_off_title`、`plateau_conflation.osm_layer_off`、`plateau_conflation.dont_show_again` の 3 つです。
+- 試験を動かす前に `npm run build` と `npm run dist` を実行します。実行しないと古い版を測ります。
+- 起点での試験は 770 件成功、5 件スキップ、失敗 0 件です。
+- 日本語のコメントはですます調では書かず、既存のコメントの文体に合わせます。
+
+---
+
+### Task 1: 判定の側を、出来事を 1 回発生させるだけにする
+
+**ファイル:**
+- 変更: `modules/services/PlateauService.js`(47 行目、340 行目から 350 行目、390 行目から 408 行目)
+- 試験: `test/browser/services/PlateauService.test.js`(777 行目から 820 行目)
+
+**やり取りする名前:**
+- 提供するもの: `PlateauService` が `osmlayeroff` を発生させます。引数はありません。ページを開き直すまでに 1 回だけです。
+- 消えるもの: `PlateauService.prototype._notifyOsmLayerOff` を削除します。以後どこからも呼びません。
+
+- [ ] **手順 1: 落ちる試験を書く**
+
+`test/browser/services/PlateauService.test.js` の 777 行目から 820 行目、つまり `// 候補が出ない理由を利用者に伝える。` で始まるコメントから `it('tells the user again after the layer is switched on and off', ...)` の終わりまでを、次の内容で置き換えます。
+
+```js
+ // 候補を伏せた理由がレイヤーの消灯であることを、一度だけ知らせる。
+ // 画面に何を出すかは `UiPlateauOsmLayerOffDialog` が決める。
+ function countOsmLayerOff(service) {
+ const seen = { count: 0 };
+ service.on('osmlayeroff', () => { seen.count++; });
+ return seen;
+ }
+
+ it('announces the switched-off OSM layer', () => {
+ const seen = countOsmLayerOff(_service);
+ setOsmState(_service, { layerEnabled: false });
+ _service.getData('ds1');
+ expect(seen.count).to.equal(1);
+ });
+
+ it('announces it only once while the layer stays switched off', () => {
+ const seen = countOsmLayerOff(_service);
+ setOsmState(_service, { layerEnabled: false });
+ _service.getData('ds1');
+ _service.getData('ds1');
+ expect(seen.count).to.equal(1, '同じ状態で何度も知らせない');
+ });
+
+ it('does not announce it again after the layer is switched on and off', () => {
+ const seen = countOsmLayerOff(_service);
+ setOsmState(_service, { layerEnabled: false });
+ _service.getData('ds1');
+ setOsmState(_service, { layerEnabled: true });
+ _service.getData('ds1');
+ setOsmState(_service, { layerEnabled: false });
+ _service.getData('ds1');
+ expect(seen.count).to.equal(1, 'ページを開き直すまでは 1 回だけ');
+ });
+
+ it('stays quiet while the tiles are still loading', () => {
+ const seen = countOsmLayerOff(_service);
+ setOsmState(_service, { tilesLoaded: false });
+ _service.getData('ds1');
+ expect(seen.count).to.equal(0);
+ });
+```
+
+`_service` は一番外側の `beforeEach`(27 行目)で毎回作り直されるので、`on()` で足した受け取り口と `_osmLayerOffNotified` は試験ごとに初期化されます。
+
+- [ ] **手順 2: 落ちることを確かめる**
+
+実行:
+
+```bash
+cd /Users/nyampire/git/Rapid && npm run build && npm run dist && npx karma start karma.conf.cjs --single-run
+```
+
+期待する結果: `announces it only once while the layer stays switched off` と `does not announce it again after the layer is switched on and off` が失敗します。前者は `osmlayeroff` を誰も発生させないため `0` になり、後者も `0` になります。
+
+- [ ] **手順 3: 出来事を発生させる**
+
+`modules/services/PlateauService.js` の 46 行目から 47 行目を次のように書き換えます。
+
+変更前:
+
+```js
+ // OSM のレイヤーが消えている件を伝えたかどうか。レイヤーが戻ると false に戻す。
+ this._osmLayerOffNotified = false;
+```
+
+変更後:
+
+```js
+ // OSM のレイヤーが消えている件を知らせたかどうか。ページを開き直すまで戻さない。
+ this._osmLayerOffNotified = false;
+```
+
+次に `getData()` の中(340 行目付近)を書き換えます。
+
+変更前:
+
+```js
+ const missing = this._osmDataMissing();
+ if (missing) {
+ if (missing === 'layer-off') this._notifyOsmLayerOff();
+ return [];
+ }
+```
+
+変更後:
+
+```js
+ const missing = this._osmDataMissing();
+ if (missing) {
+ if (missing === 'layer-off' && !this._osmLayerOffNotified) {
+ this._osmLayerOffNotified = true;
+ this.emit('osmlayeroff');
+ }
+ return [];
+ }
+```
+
+次に `_osmDataMissing()` の中から、印を戻す 1 行を削除します。
+
+変更前:
+
+```js
+ const layer = this.context.systems.gfx?.scene?.layers?.get('osm');
+ if (layer && layer.enabled === false) return 'layer-off';
+ this._osmLayerOffNotified = false;
+```
+
+変更後:
+
+```js
+ const layer = this.context.systems.gfx?.scene?.layers?.get('osm');
+ if (layer && layer.enabled === false) return 'layer-off';
+```
+
+最後に `_notifyOsmLayerOff()` を、その直前の説明コメントごと削除します。削除する範囲は次のとおりです。
+
+```js
+ /**
+ * _notifyOsmLayerOff
+ * 候補が出ない理由を利用者に伝える。
+ * レイヤーが消えたままなのは利用者が直せる状態なので伝える。
+ * タイルの取得は待てば終わるので伝えない。
+ * 同じ状態が続くあいだは一度だけ出し、レイヤーが戻ったときに出し直せるようにする。
+ */
+ _notifyOsmLayerOff() {
+ if (this._osmLayerOffNotified) return;
+ this._osmLayerOffNotified = true;
+
+ const flash = this.context.systems.ui?.Flash;
+ if (typeof flash !== 'function') return;
+
+ const l10n = this.context.systems.l10n;
+ const key = 'plateau_conflation.osm_layer_off';
+ flash.duration(5000).label(l10n ? l10n.t(key) : key);
+ flash();
+ }
+```
+
+- [ ] **手順 4: 通ることを確かめる**
+
+実行:
+
+```bash
+cd /Users/nyampire/git/Rapid && npm run build && npm run dist && npx karma start karma.conf.cjs --single-run
+```
+
+期待する結果: 771 件成功、5 件スキップ、失敗 0 件。試験の数は、消した 3 件と足した 4 件の差で 1 件増えます。
+
+- [ ] **手順 5: 記録する**
+
+```bash
+cd /Users/nyampire/git/Rapid
+git add modules/services/PlateauService.js test/browser/services/PlateauService.test.js
+git commit -m "$(cat <<'EOF'
+refactor(plateau): レイヤー消灯の知らせを出来事に変える
+
+画面下端の通知を出す処理を削除し、候補を伏せた理由がレイヤーの消灯で
+あることを osmlayeroff として 1 回だけ発生させる。何を画面に出すかは
+受け取る側が決める。
+
+Co-Authored-By: Claude Opus 5
+EOF
+)"
+```
+
+---
+
+### Task 2: ダイアログを出す部品を作る
+
+**ファイル:**
+- 作成: `modules/ui/UiPlateauOsmLayerOffDialog.js`
+- 変更: `modules/ui/index.js`(55 行目の次)
+- 変更: `data/core.yaml`(28 行目の次)
+- 変更: `data/l10n/core.en.json`(31 行目)
+- 変更: `data/l10n/core.ja.json`(2966 行目)
+- 試験: `test/browser/ui/UiPlateauOsmLayerOffDialog.js`(作成)
+
+**やり取りする名前:**
+- 使うもの: Task 1 の `osmlayeroff`。`context.services.plateau.on('osmlayeroff', fn)` で受け取ります。
+- 提供するもの: `UiPlateauOsmLayerOffDialog` という class。`new UiPlateauOsmLayerOffDialog(context)` で組み立て、引数を取らない `show()` を持ちます。`show()` はダイアログの d3 選択を返すか、出さなかった場合は `undefined` を返します。
+
+- [ ] **手順 1: 文言を足す**
+
+`data/core.yaml` の 28 行目の次に、2 行を足します。足したあとは次の形になります。
+
+```yaml
+ plateau_conflation:
+ osm_layer_off: "Plateau buildings stay hidden while the OpenStreetMap data layer is off, because existing buildings cannot be checked for duplicates. Press Shift+O to turn it back on."
+ osm_layer_off_title: PLATEAU buildings are hidden
+ dont_show_again: Do not show this again
+```
+
+`data/l10n/core.en.json` の 30 行目から 32 行目を、次の形にします。
+
+```json
+ "plateau_conflation": {
+ "osm_layer_off": "Plateau buildings stay hidden while the OpenStreetMap data layer is off, because existing buildings cannot be checked for duplicates. Press Shift+O to turn it back on.",
+ "osm_layer_off_title": "PLATEAU buildings are hidden",
+ "dont_show_again": "Do not show this again"
+ },
+```
+
+`data/l10n/core.ja.json` の 2965 行目から 2967 行目を、次の形にします。
+
+```json
+ "plateau_conflation": {
+ "osm_layer_off": "OpenStreetMap のデータのレイヤーが消えているあいだは、Plateau の建物を表示しません。既存の建物と重なるかを確かめられないためです。Shift+O で戻せます。",
+ "osm_layer_off_title": "PLATEAU の建物を表示していません",
+ "dont_show_again": "次から表示しない"
+ },
+```
+
+- [ ] **手順 2: 落ちる試験を書く**
+
+`test/browser/ui/UiPlateauOsmLayerOffDialog.js` を作り、次の内容を書きます。
+
+```js
+describe('UiPlateauOsmLayerOffDialog', () => {
+ const HIDDEN_KEY = 'plateau.osm-layer-off-dialog.hidden';
+ let elem;
+
+ class MockLocalizationSystem {
+ constructor() { }
+ initAsync() { return Promise.resolve(); }
+ t(id) { return id; }
+ tHtml(id) { return id; }
+ }
+
+ class MockStorageSystem {
+ constructor() { this._map = new Map(); }
+ getItem(k) { return this._map.has(k) ? this._map.get(k) : null; }
+ setItem(k, v) { this._map.set(k, v); return true; }
+ removeItem(k) { this._map.delete(k); }
+ }
+
+ class MockPlateauService {
+ constructor() { this._handlers = new Map(); }
+ on(type, fn) {
+ if (!this._handlers.has(type)) this._handlers.set(type, []);
+ this._handlers.get(type).push(fn);
+ return this;
+ }
+ emit(type) {
+ for (const fn of this._handlers.get(type) ?? []) fn();
+ }
+ }
+
+ class MockContext {
+ constructor() {
+ this.systems = {
+ l10n: new MockLocalizationSystem(),
+ storage: new MockStorageSystem()
+ };
+ this.services = { plateau: new MockPlateauService() };
+ }
+ container() { return elem; }
+ }
+
+ let context;
+
+ beforeEach(() => {
+ elem = d3.select('body')
+ .append('div')
+ .attr('class', 'plateau-dialog-wrap');
+ context = new MockContext();
+ });
+
+ afterEach(() => {
+ d3.select('.plateau-dialog-wrap').remove();
+ d3.selectAll('.shaded').remove();
+ });
+
+ // `no-new` を避けるために関数の戻り値として返す。
+ // この部品は組み立てた時点で `osmlayeroff` の購読を始めるので、戻り値は使わない試験が多い。
+ function mountDialog(ctx) {
+ return new Rapid.UiPlateauOsmLayerOffDialog(ctx);
+ }
+
+
+ it('opens the dialog when the service announces the switched-off layer', () => {
+ mountDialog(context);
+ context.services.plateau.emit('osmlayeroff');
+ expect(elem.selectAll('.modal').size()).to.equal(1);
+ });
+
+ it('shows the title, the reason and the checkbox', () => {
+ mountDialog(context);
+ context.services.plateau.emit('osmlayeroff');
+ expect(elem.selectAll('.modal-section.header h3').text())
+ .to.equal('plateau_conflation.osm_layer_off_title');
+ expect(elem.selectAll('.modal-section.message-text p').text())
+ .to.equal('plateau_conflation.osm_layer_off');
+ expect(elem.selectAll('.plateau-dont-show-again input').size()).to.equal(1);
+ });
+
+ it('does not open the dialog when the user asked not to see it', () => {
+ context.systems.storage.setItem(HIDDEN_KEY, 'true');
+ mountDialog(context);
+ context.services.plateau.emit('osmlayeroff');
+ expect(elem.selectAll('.modal').size()).to.equal(0);
+ });
+
+ it('remembers the choice when the checkbox is ticked', () => {
+ mountDialog(context);
+ context.services.plateau.emit('osmlayeroff');
+ const node = elem.select('.plateau-dont-show-again input').node();
+ node.checked = true;
+ happen.once(node, { type: 'change' });
+ expect(context.systems.storage.getItem(HIDDEN_KEY)).to.equal('true');
+ });
+
+ it('forgets the choice when the checkbox is unticked', () => {
+ mountDialog(context);
+ context.services.plateau.emit('osmlayeroff');
+ const node = elem.select('.plateau-dont-show-again input').node();
+ node.checked = true;
+ happen.once(node, { type: 'change' });
+ node.checked = false;
+ happen.once(node, { type: 'change' });
+ expect(context.systems.storage.getItem(HIDDEN_KEY)).to.be.null;
+ });
+});
+```
+
+最後の試験は、開いたダイアログの中でチェックを入れてから外します。
+利用者も同じ操作ができるので、試験のためだけの引数は要りません。
+
+- [ ] **手順 3: 落ちることを確かめる**
+
+実行:
+
+```bash
+cd /Users/nyampire/git/Rapid && npm run build && npm run dist && npx karma start karma.conf.cjs --single-run
+```
+
+期待する結果: `UiPlateauOsmLayerOffDialog` の 5 件がすべて失敗します。理由は `Rapid.UiPlateauOsmLayerOffDialog is not a constructor` です。
+
+- [ ] **手順 4: 部品を作る**
+
+`modules/ui/UiPlateauOsmLayerOffDialog.js` を作り、次の内容を書きます。
+
+```js
+import { uiConfirm } from './confirm.js';
+
+const HIDDEN_KEY = 'plateau.osm-layer-off-dialog.hidden';
+
+
+/**
+ * UiPlateauOsmLayerOffDialog
+ * OSM のデータのレイヤーが消えているために PLATEAU の候補を伏せたことを、
+ * 画面中央のダイアログで伝える。
+ *
+ * 出すかどうかの判断は `PlateauService` が持ち、この部品は `osmlayeroff` を
+ * 受け取るだけ。`PlateauService` はページを開き直すまでに 1 回しか発生させない。
+ *
+ * 「次から表示しない」を選んだ利用者には、以後まったく出さない。
+ */
+export class UiPlateauOsmLayerOffDialog {
+
+ /**
+ * @constructor
+ * @param `context` Global shared application context
+ */
+ constructor(context) {
+ this.context = context;
+
+ // Ensure methods used as callbacks always have `this` bound correctly.
+ this.show = this.show.bind(this);
+
+ const plateau = context.services?.plateau;
+ if (plateau) {
+ plateau.on('osmlayeroff', this.show);
+ }
+ }
+
+
+ /**
+ * show
+ * ダイアログを開く。「次から表示しない」を選んだ利用者には開かない。
+ * @return {d3-selection?} 開いたダイアログ。開かなかった場合は undefined
+ */
+ show() {
+ const context = this.context;
+ const storage = context.systems.storage;
+ const l10n = context.systems.l10n;
+
+ if (storage?.getItem(HIDDEN_KEY) === 'true') return;
+
+ const $modal = uiConfirm(context, context.container());
+
+ $modal.select('.modal-section.header')
+ .append('h3')
+ .text(l10n.t('plateau_conflation.osm_layer_off_title'));
+
+ const $message = $modal.select('.modal-section.message-text');
+
+ $message
+ .append('p')
+ .text(l10n.t('plateau_conflation.osm_layer_off'));
+
+ const $label = $message
+ .append('label')
+ .attr('class', 'plateau-dont-show-again');
+
+ $label
+ .append('input')
+ .attr('type', 'checkbox')
+ .on('change', function() {
+ if (this.checked) {
+ storage?.setItem(HIDDEN_KEY, 'true');
+ } else {
+ storage?.removeItem(HIDDEN_KEY);
+ }
+ });
+
+ $label
+ .append('span')
+ .text(l10n.t('plateau_conflation.dont_show_again'));
+
+ $modal.okButton();
+
+ return $modal;
+ }
+
+}
+```
+
+`modules/ui/index.js` の 55 行目(`export { UiPhotoViewer } ...`)の次に、次の 1 行を足します。
+
+```js
+export { UiPlateauOsmLayerOffDialog } from './UiPlateauOsmLayerOffDialog.js';
+```
+
+- [ ] **手順 5: 通ることを確かめる**
+
+実行:
+
+```bash
+cd /Users/nyampire/git/Rapid && npx eslint modules test && npm run build && npm run dist && npx karma start karma.conf.cjs --single-run
+```
+
+期待する結果: 静的検査はエラー 0 件、警告 42 件。試験は 776 件成功、5 件スキップ、失敗 0 件。Task 1 の 771 件に、この課題の 5 件が足されます。
+
+試験を足す課題では、試験だけでなく静的検査も実行します。
+この計画の初版では Task 2 に静的検査を入れておらず、`no-new` の違反を 5 件見落としました。
+
+- [ ] **手順 6: 記録する**
+
+```bash
+cd /Users/nyampire/git/Rapid
+git add modules/ui/UiPlateauOsmLayerOffDialog.js modules/ui/index.js \
+ data/core.yaml data/l10n/core.en.json data/l10n/core.ja.json \
+ test/browser/ui/UiPlateauOsmLayerOffDialog.js
+git commit -m "$(cat <<'EOF'
+feat(plateau): レイヤー消灯の理由をダイアログで出す部品を足す
+
+osmlayeroff を受け取り、見出しと理由と「次から表示しない」を持つ
+ダイアログを開く。印は StorageSystem に保存し、選ばれていれば開かない。
+
+Co-Authored-By: Claude Opus 5
+EOF
+)"
+```
+
+---
+
+### Task 3: 部品を組み立てて、画面でつながるようにする
+
+**ファイル:**
+- 変更: `modules/core/UiSystem.js`(8 行目から 11 行目の読み込み、100 行目付近の組み立て)
+
+**やり取りする名前:**
+- 使うもの: Task 2 の `UiPlateauOsmLayerOffDialog`。
+- 提供するもの: `context.systems.ui.PlateauOsmLayerOffDialog`。他の場所からは参照しません。
+
+- [ ] **手順 1: 読み込みに足す**
+
+`modules/core/UiSystem.js` の 7 行目から 11 行目を、次の形にします。
+
+変更前:
+
+```js
+import {
+ UiApiStatus, UiDefs, uiEditMenu, uiFlash, UiFullscreen, uiIntro,
+ uiLoading, UiMapFooter, UiMapToolbar, uiMapRouletteMenu, UiOvermap,
+ uiSplash, uiRestore, UiShortcuts, UiSidebar, uiWhatsNew
+} from '../ui/index.js';
+```
+
+変更後:
+
+```js
+import {
+ UiApiStatus, UiDefs, uiEditMenu, uiFlash, UiFullscreen, uiIntro,
+ uiLoading, UiMapFooter, UiMapToolbar, uiMapRouletteMenu, UiOvermap,
+ UiPlateauOsmLayerOffDialog, uiSplash, uiRestore, UiShortcuts, UiSidebar,
+ uiWhatsNew
+} from '../ui/index.js';
+```
+
+- [ ] **手順 2: 組み立てに足す**
+
+`modules/core/UiSystem.js` の `this.Overmap = new UiOvermap(context);` の次の行に、1 行を足します。足したあとは次の形になります。
+
+```js
+ this.Overmap = new UiOvermap(context);
+ this.PlateauOsmLayerOffDialog = new UiPlateauOsmLayerOffDialog(context);
+ this.Shortcuts = new UiShortcuts(context);
+```
+
+描画処理には手を入れません。この部品は出来事を受け取ったときだけ画面に要素を足します。
+
+あわせて、`UiSystem` の constructor にある持ち物の一覧に `this.PlateauOsmLayerOffDialog = null;` を足します。
+このファイルでは、`initAsync` で組み立てる部品をすべて constructor で `null` にしてから代入しており、それに合わせます。
+(この 1 行は計画の初版に書き漏らしていました。実装後に追記しています。)
+
+- [ ] **手順 3: 静的検査と試験を通す**
+
+実行:
+
+```bash
+cd /Users/nyampire/git/Rapid && npm run lint && npm run build && npm run dist && npx karma start karma.conf.cjs --single-run
+```
+
+期待する結果: `npm run lint` はエラー 0 件です。警告は起点と同じ 42 件で、すべて元からある todo コメントです。試験は 776 件成功、5 件スキップ、失敗 0 件です。
+
+- [ ] **手順 4: 画面で確かめる**
+
+リポジトリに入っている開発用サーバを起動します。`scripts/server.js` がリポジトリの根をポート 8080 で配信します。
+
+```bash
+cd /Users/nyampire/git/Rapid && npm run start:server
+```
+
+ブラウザで `http://localhost:8080/dist/#map=17.50/35.64780/139.70200` を開き、次の 3 点を確かめます。
+
+1. `Shift` + `O` で OSM のデータのレイヤーを消すと、ダイアログが 1 回出ること
+2. OK で閉じたあと、もう一度 `Shift` + `O` を 2 回押しても、ダイアログが出ないこと
+3. 「次から表示しない」を選んで閉じ、ページを開き直してから `Shift` + `O` を押しても、ダイアログが出ないこと
+
+3 を試したあとは、開発者コンソールで印を消して元に戻します。
+
+```js
+localStorage.removeItem('plateau.osm-layer-off-dialog.hidden');
+```
+
+この確認は自動の試験では代えられません。`PlateauService` と画面部品のつなぎ込みが実際に動くかは、組み立てた `UiSystem` の上でしか分からないためです。
+
+- [ ] **手順 5: 記録する**
+
+```bash
+cd /Users/nyampire/git/Rapid
+git add modules/core/UiSystem.js
+git commit -m "$(cat <<'EOF'
+feat(plateau): レイヤー消灯のダイアログを画面に組み込む
+
+UiSystem で 1 度だけ組み立てる。描画処理には手を入れない。
+
+Co-Authored-By: Claude Opus 5
+EOF
+)"
+```
+
+---
+
+## 完了の条件
+
+- `npm run lint` がエラー 0 件で通ること
+- 試験が 776 件成功、5 件スキップ、失敗 0 件であること
+- Task 3 手順 4 の 3 点が画面で確かめられていること
+- `_notifyOsmLayerOff` がコードのどこにも残っていないこと(`grep -rn "_notifyOsmLayerOff" modules test` が 0 件)
diff --git a/docs/superpowers/specs/2026-09-08-plateau-osm-layer-off-dialog-design.ja.md b/docs/superpowers/specs/2026-09-08-plateau-osm-layer-off-dialog-design.ja.md
new file mode 100644
index 000000000..36a128570
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-08-plateau-osm-layer-off-dialog-design.ja.md
@@ -0,0 +1,135 @@
+# OSM のデータのレイヤーが消えている理由をダイアログで伝える設計
+
+- 日付: 2026-09-08
+- ブランチ: `feature/plateau-osm-layer-off-notice`
+- 起点: `fb97052fc`(Pull Request #52 のマージ地点)
+
+## 背景
+
+Pull Request #52 で、重複の判定に使う OSM の建物が編集ソフトの中に無いあいだは、PLATEAU の候補を出さないようにしました。
+OSM のデータのレイヤーが消えている場合は、利用者が `Shift` + `O` で戻せる状態なので、その理由を画面に出しています。
+
+この文言を、画面下端に一時的に出る通知で表示しています。
+表示は 5 秒で終わり、レイヤーが戻ったかどうかは見ていません。
+
+利用者から 2 つの指摘がありました。
+レイヤーを戻しても文言がその場で消えないこと、そして通知よりも見落としにくい形で伝えてほしいことです。
+
+実機で確認したところ、文言は 5 秒後に画面から外れました。
+残り続ける不具合ではなく、レイヤーの状態と表示の寿命が結び付いていないことが原因です。
+
+## 決めたこと
+
+画面下端の通知をやめて、画面中央のダイアログで伝えます。
+
+ダイアログはページを開き直すごとに 1 回だけ出します。
+「次から表示しない」を選んだ場合は、以後ずっと出しません。
+
+## 検討して採らなかった案
+
+### 画面下端に帯を出し続ける案
+
+OSM の API が使えないときに出る赤い帯と同じ仕組みで、レイヤーが消えているあいだ文言を出し続ける案です。
+レイヤーが戻った時点で消えるため、指摘の 1 つ目をそのまま満たします。
+
+採りませんでした。
+背景を暗くしない分だけ見落とされやすく、指摘の 2 つ目に応えられないためです。
+
+### レイヤーを消すたびにダイアログを出す案
+
+見落とす可能性は最も低くなります。
+
+採りませんでした。
+レイヤーの切り替えは `Shift` + `O` の 1 打で起きるため、意図して消した人の作業が毎回止まります。
+
+## 設計
+
+### 1. 判定の側から表示の責任を外す
+
+`PlateauService` は「候補を伏せた理由がレイヤーの消灯である」ことを一度だけ通知し、画面に何を出すかは決めません。
+
+`_notifyOsmLayerOff()` と、そこから呼んでいる画面下端の通知を削除します。
+代わりに `getData()` の中で `osmlayeroff` という出来事を 1 回だけ発生させます。
+1 回に限る判断には、既存の `_osmLayerOffNotified` をそのまま使います。
+`false` のときだけ出来事を発生させ、同時に `true` にします。
+
+`PlateauService` は `AbstractSystem` を継承しており、`AbstractSystem` は `EventEmitter` なので、追加の仕組みは要りません。
+
+`_osmDataMissing()` の中にある `this._osmLayerOffNotified = false;` の行を削除します。
+この行は「レイヤーが戻ったらもう一度出せるようにする」ためのものでしたが、ページを開き直すごとに 1 回という方針では不要です。
+削除すると `_osmDataMissing()` は状態を変えない問い合わせだけになり、読み違えの余地が減ります。
+
+候補を伏せる判定そのものは変えません。
+タイルが未取得のときに何も出さない扱いも変えません。
+
+### 2. ダイアログを出す部品を新しく作る
+
+`modules/ui/UiPlateauOsmLayerOffDialog.js` を追加します。
+
+この部品は組み立て時に `context.services.plateau` の `osmlayeroff` を購読します。
+出来事を受け取ったら、保存された設定を読み、「次から表示しない」が選ばれていなければダイアログを開きます。
+
+ダイアログは既存の `uiConfirm` を使います。
+`uiConfirm` は見出しと本文と OK ボタンを持つ形で、`uiModal` を土台にしています。
+本文の下にチェックボックスを 1 つ足します。
+
+チェックボックスが選ばれたら `context.systems.storage` に印を保存し、外されたら消します。
+保存する名前は `plateau.osm-layer-off-dialog.hidden` とします。
+
+### 3. 部品を組み立てる場所
+
+`UiSystem` の初期化処理で、他の画面部品と並べて 1 度だけ組み立てます。
+読み込みの 1 行と組み立ての 1 行を足すだけで、描画処理には手を入れません。
+
+`UiApiStatus` も組み立て時に `context.services.osm` を参照しており、同じ順序で動きます。
+
+### 4. 文言
+
+既存の `plateau_conflation.osm_layer_off` を本文として使い回します。
+見出しと、チェックボックスの説明を新しく足します。
+
+| 名前 | 用途 |
+|---|---|
+| `plateau_conflation.osm_layer_off_title` | ダイアログの見出し |
+| `plateau_conflation.osm_layer_off` | 本文(既存のものを使う) |
+| `plateau_conflation.dont_show_again` | チェックボックスの説明 |
+
+`data/core.yaml`、`data/l10n/core.en.json`、`data/l10n/core.ja.json` の 3 つに、日本語と英語の両方を書きます。
+
+## この設計の弱点
+
+「次から表示しない」を選んだ利用者には、以後この理由が一切伝わりません。
+数か月後に同じ状況に陥ったとき、候補が出ない理由を自力で探すことになります。
+
+保存する印はブラウザごとに残ります。
+別の端末や別のブラウザで開いた場合は、もう一度ダイアログが出ます。
+
+ページを開いているあいだに 2 回目以降レイヤーを消しても、何も出ません。
+1 回目のダイアログを読み飛ばした利用者は、その回の作業中に理由を知る手段がありません。
+
+## 試験
+
+### `PlateauService` の試験
+
+いまある画面下端の通知に関する試験 3 件を削除し、次の 4 件に置き換えます。
+
+- レイヤーが消えているとき、`osmlayeroff` が発生すること
+- 同じ状態が続いても、`osmlayeroff` は 1 回しか発生しないこと
+- レイヤーが戻ってからもう一度消しても、`osmlayeroff` は再発しないこと
+- タイルが未取得のときは `osmlayeroff` が発生しないこと
+
+候補を伏せる判定の試験は、いまのまま残します。
+
+### ダイアログの部品の試験
+
+- `osmlayeroff` を受け取るとダイアログが開くこと
+- 「次から表示しない」が保存されているとき、ダイアログが開かないこと
+- チェックボックスを選ぶと設定が保存されること
+- チェックボックスを外すと設定が消えること
+
+### 実行
+
+`npm run build` と `npm run dist` を実行してから `npm run test:browser` を実行します。
+作り直さないと古い版を測ります。
+
+起点の `fb97052fc` では 770 件成功、5 件スキップ、失敗 0 件です。
diff --git a/modules/core/UiSystem.js b/modules/core/UiSystem.js
index 9da9b7e8b..1bd971fcd 100644
--- a/modules/core/UiSystem.js
+++ b/modules/core/UiSystem.js
@@ -7,7 +7,8 @@ import { utilDetect } from '../util/detect.js';
import {
UiApiStatus, UiDefs, uiEditMenu, uiFlash, UiFullscreen, uiIntro,
uiLoading, UiMapFooter, UiMapToolbar, uiMapRouletteMenu, UiOvermap,
- uiSplash, uiRestore, UiShortcuts, UiSidebar, uiWhatsNew
+ UiPlateauOsmLayerOffDialog, uiSplash, uiRestore, UiShortcuts, UiSidebar,
+ uiWhatsNew
} from '../ui/index.js';
@@ -45,6 +46,7 @@ export class UiSystem extends AbstractSystem {
this.MapFooter = null;
this.MapToolbar = null;
this.Overmap = null;
+ this.PlateauOsmLayerOffDialog = null;
this.Shortcuts = null;
this.Sidebar = null;
@@ -107,6 +109,7 @@ export class UiSystem extends AbstractSystem {
this.MapFooter = new UiMapFooter(context);
this.MapToolbar = new UiMapToolbar(context);
this.Overmap = new UiOvermap(context);
+ this.PlateauOsmLayerOffDialog = new UiPlateauOsmLayerOffDialog(context);
this.Shortcuts = new UiShortcuts(context);
this.Sidebar = new UiSidebar(context);
diff --git a/modules/services/PlateauService.js b/modules/services/PlateauService.js
index 20c0509b5..ff974a9e6 100644
--- a/modules/services/PlateauService.js
+++ b/modules/services/PlateauService.js
@@ -27,6 +27,8 @@ const TILEZOOM = 16;
*
* Events available:
* `loadedData`
+ * `osmlayeroff` OSM のデータのレイヤーが消えているために候補を伏せたときに発生する。
+ * 引数は無い。ページを開き直すまでに 1 回しか発生しない。
*/
export class PlateauService extends AbstractSystem {
@@ -48,7 +50,7 @@ export class PlateauService extends AbstractSystem {
rejected: new Set() // Set(entityID) - overlapping with OSM
};
- // OSM のレイヤーが消えている件を伝えたかどうか。レイヤーが戻ると false に戻す。
+ // OSM のレイヤーが消えている件を知らせたかどうか。ページを開き直すまで戻さない。
this._osmLayerOffNotified = false;
// Cache for coverage area GeoJSON (loaded once, used by PixiLayerPlateauCoverage)
@@ -342,7 +344,12 @@ export class PlateauService extends AbstractSystem {
if (useConflationStr !== 'false' && useConflationStr !== 'no') {
const missing = this._osmDataMissing();
if (missing) {
- if (missing === 'layer-off') this._notifyOsmLayerOff();
+ if (missing === 'layer-off' && !this._osmLayerOffNotified) {
+ this._osmLayerOffNotified = true;
+ // `getData` は Pixi の描画の途中で呼ばれる。受け取り側がここで例外を投げると
+ // 描画の繰り返しがそのページの間止まるため、描画の外に出してから知らせる。
+ window.setTimeout(() => this.emit('osmlayeroff'), 0);
+ }
return [];
}
entities = this._filterPlateauOverlaps(entities, ds.graph);
@@ -368,7 +375,6 @@ export class PlateauService extends AbstractSystem {
_osmDataMissing() {
const layer = this.context.systems.gfx?.scene?.layers?.get('osm');
if (layer && layer.enabled === false) return 'layer-off';
- this._osmLayerOffNotified = false;
// タイルの取得に失敗したまま再取得されない経路もあるため、取得済みかどうかも見る。
// 取得済みの一覧は上流のファイルの持ち物で、上流を取り込んだときに形が変わりうる。
@@ -383,27 +389,6 @@ export class PlateauService extends AbstractSystem {
}
- /**
- * _notifyOsmLayerOff
- * 候補が出ない理由を利用者に伝える。
- * レイヤーが消えたままなのは利用者が直せる状態なので伝える。
- * タイルの取得は待てば終わるので伝えない。
- * 同じ状態が続くあいだは一度だけ出し、レイヤーが戻ったときに出し直せるようにする。
- */
- _notifyOsmLayerOff() {
- if (this._osmLayerOffNotified) return;
- this._osmLayerOffNotified = true;
-
- const flash = this.context.systems.ui?.Flash;
- if (typeof flash !== 'function') return;
-
- const l10n = this.context.systems.l10n;
- const key = 'plateau_conflation.osm_layer_off';
- flash.duration(5000).label(l10n ? l10n.t(key) : key);
- flash();
- }
-
-
/**
* loadTiles
* Schedule any data requests needed to cover the current map view
diff --git a/modules/ui/UiPlateauOsmLayerOffDialog.js b/modules/ui/UiPlateauOsmLayerOffDialog.js
new file mode 100644
index 000000000..7e1e80b6c
--- /dev/null
+++ b/modules/ui/UiPlateauOsmLayerOffDialog.js
@@ -0,0 +1,83 @@
+import { uiConfirm } from './confirm.js';
+
+const HIDDEN_KEY = 'plateau.osm-layer-off-dialog.hidden';
+
+
+/**
+ * UiPlateauOsmLayerOffDialog
+ * OSM のデータのレイヤーが消えているために PLATEAU の候補を伏せたことを、
+ * 画面中央のダイアログで伝える。
+ *
+ * 出すかどうかの判断は `PlateauService` が持ち、この部品は `osmlayeroff` を
+ * 受け取るだけ。`PlateauService` はページを開き直すまでに 1 回しか発生させない。
+ *
+ * 「次から表示しない」を選んだ利用者には、以後まったく出さない。
+ */
+export class UiPlateauOsmLayerOffDialog {
+
+ /**
+ * @constructor
+ * @param `context` Global shared application context
+ */
+ constructor(context) {
+ this.context = context;
+
+ // Ensure methods used as callbacks always have `this` bound correctly.
+ this.show = this.show.bind(this);
+
+ const plateau = context.services?.plateau;
+ if (plateau) {
+ plateau.on('osmlayeroff', this.show);
+ }
+ }
+
+
+ /**
+ * show
+ * ダイアログを開く。「次から表示しない」を選んだ利用者には開かない。
+ * @return {d3-selection?} 開いたダイアログ。開かなかった場合は undefined
+ */
+ show() {
+ const context = this.context;
+ const storage = context.systems.storage;
+ const l10n = context.systems.l10n;
+
+ if (storage?.getItem(HIDDEN_KEY) === 'true') return;
+
+ const $modal = uiConfirm(context, context.container());
+
+ $modal.select('.modal-section.header')
+ .append('h3')
+ .text(l10n.t('plateau_conflation.osm_layer_off_title'));
+
+ const $message = $modal.select('.modal-section.message-text');
+
+ $message
+ .append('p')
+ .text(l10n.t('plateau_conflation.osm_layer_off'));
+
+ const $label = $message
+ .append('label')
+ .attr('class', 'plateau-dont-show-again');
+
+ $label
+ .append('input')
+ .attr('type', 'checkbox')
+ .on('change', function() {
+ if (this.checked) {
+ storage?.setItem(HIDDEN_KEY, 'true');
+ } else {
+ storage?.removeItem(HIDDEN_KEY);
+ }
+ });
+
+ $label
+ .append('span')
+ .text(l10n.t('plateau_conflation.dont_show_again'));
+
+ $modal.okButton();
+
+ return $modal;
+ }
+
+}
diff --git a/modules/ui/index.js b/modules/ui/index.js
index 76144e1d0..9dfb20070 100644
--- a/modules/ui/index.js
+++ b/modules/ui/index.js
@@ -53,6 +53,7 @@ export { uiOsmoseHeader } from './osmose_header.js';
export { UiOvermap } from './UiOvermap.js';
export { uiPane } from './pane.js';
export { UiPhotoViewer } from './UiPhotoViewer.js';
+export { UiPlateauOsmLayerOffDialog } from './UiPlateauOsmLayerOffDialog.js';
export { uiPopover } from './popover.js';
export { uiPresetIcon } from './preset_icon.js';
export { uiPresetList } from './preset_list.js';
diff --git a/test/browser/services/PlateauService.test.js b/test/browser/services/PlateauService.test.js
index 99306347f..1cf9f2256 100644
--- a/test/browser/services/PlateauService.test.js
+++ b/test/browser/services/PlateauService.test.js
@@ -774,49 +774,54 @@ describe('PlateauService', () => {
expect(ways).to.have.lengthOf(1);
});
- // 候補が出ない理由を利用者に伝える。
- // レイヤーを消したままなのは利用者が直せる状態なので伝える。
- // タイルの取得は待てば終わるので伝えない。
- function mockFlash() {
- const f = () => { f.calls.push(f._label); return f; };
- f.calls = [];
- f.duration = () => f;
- f.label = (t) => { f._label = t; return f; };
- return f;
+ // 候補を伏せた理由がレイヤーの消灯であることを、一度だけ知らせる。
+ // 画面に何を出すかは `UiPlateauOsmLayerOffDialog` が決める。
+ function countOsmLayerOff(service) {
+ const seen = { count: 0 };
+ service.on('osmlayeroff', () => { seen.count++; });
+ return seen;
}
- function withUi(service) {
- const flash = mockFlash();
- service.context.systems.ui = { Flash: flash };
- service.context.systems.l10n = { t: (k) => k };
- return flash;
+ // 出来事は描画の外に出してから発生するので、1 拍待ってから数える。
+ function nextTurn() {
+ return new Promise(resolve => { window.setTimeout(resolve, 0); });
}
- it('tells the user once while the OSM layer stays switched off', () => {
- const flash = withUi(_service);
+ it('announces the switched-off OSM layer', async () => {
+ const seen = countOsmLayerOff(_service);
setOsmState(_service, { layerEnabled: false });
_service.getData('ds1');
- _service.getData('ds1');
- expect(flash.calls).to.have.lengthOf(1, '同じ状態で何度も出さない');
- expect(flash.calls[0]).to.equal('plateau_conflation.osm_layer_off');
+ await nextTurn();
+ expect(seen.count).to.equal(1);
});
- it('stays quiet while the tiles are still loading', () => {
- const flash = withUi(_service);
- setOsmState(_service, { tilesLoaded: false });
+ it('announces it only once while the layer stays switched off', async () => {
+ const seen = countOsmLayerOff(_service);
+ setOsmState(_service, { layerEnabled: false });
+ _service.getData('ds1');
_service.getData('ds1');
- expect(flash.calls).to.have.lengthOf(0);
+ await nextTurn();
+ expect(seen.count).to.equal(1, '同じ状態で何度も知らせない');
});
- it('tells the user again after the layer is switched on and off', () => {
- const flash = withUi(_service);
+ it('does not announce it again after the layer is switched on and off', async () => {
+ const seen = countOsmLayerOff(_service);
setOsmState(_service, { layerEnabled: false });
_service.getData('ds1');
setOsmState(_service, { layerEnabled: true });
_service.getData('ds1');
setOsmState(_service, { layerEnabled: false });
_service.getData('ds1');
- expect(flash.calls).to.have.lengthOf(2);
+ await nextTurn();
+ expect(seen.count).to.equal(1, 'ページを開き直すまでは 1 回だけ');
+ });
+
+ it('stays quiet while the tiles are still loading', async () => {
+ const seen = countOsmLayerOff(_service);
+ setOsmState(_service, { tilesLoaded: false });
+ _service.getData('ds1');
+ await nextTurn();
+ expect(seen.count).to.equal(0);
});
});
diff --git a/test/browser/ui/UiPlateauOsmLayerOffDialog.js b/test/browser/ui/UiPlateauOsmLayerOffDialog.js
new file mode 100644
index 000000000..9350ba952
--- /dev/null
+++ b/test/browser/ui/UiPlateauOsmLayerOffDialog.js
@@ -0,0 +1,105 @@
+describe('UiPlateauOsmLayerOffDialog', () => {
+ const HIDDEN_KEY = 'plateau.osm-layer-off-dialog.hidden';
+ let elem;
+
+ class MockLocalizationSystem {
+ constructor() { }
+ initAsync() { return Promise.resolve(); }
+ t(id) { return id; }
+ tHtml(id) { return id; }
+ }
+
+ class MockStorageSystem {
+ constructor() { this._map = new Map(); }
+ getItem(k) { return this._map.has(k) ? this._map.get(k) : null; }
+ setItem(k, v) { this._map.set(k, v); return true; }
+ removeItem(k) { this._map.delete(k); }
+ }
+
+ class MockPlateauService {
+ constructor() { this._handlers = new Map(); }
+ on(type, fn) {
+ if (!this._handlers.has(type)) this._handlers.set(type, []);
+ this._handlers.get(type).push(fn);
+ return this;
+ }
+ emit(type) {
+ for (const fn of this._handlers.get(type) ?? []) fn();
+ }
+ }
+
+ class MockContext {
+ constructor() {
+ this.systems = {
+ l10n: new MockLocalizationSystem(),
+ storage: new MockStorageSystem()
+ };
+ this.services = { plateau: new MockPlateauService() };
+ }
+ container() { return elem; }
+ }
+
+ let context;
+
+ // `no-new` を避けるために関数の戻り値として返す。
+ // この部品は組み立てた時点で `osmlayeroff` の購読を始めるので、戻り値は使わない試験が多い。
+ function mountDialog(ctx) {
+ return new Rapid.UiPlateauOsmLayerOffDialog(ctx);
+ }
+
+ beforeEach(() => {
+ elem = d3.select('body')
+ .append('div')
+ .attr('class', 'plateau-dialog-wrap');
+ context = new MockContext();
+ });
+
+ afterEach(() => {
+ d3.select('.plateau-dialog-wrap').remove();
+ d3.selectAll('.shaded').remove();
+ });
+
+
+ it('opens the dialog when the service announces the switched-off layer', () => {
+ mountDialog(context);
+ context.services.plateau.emit('osmlayeroff');
+ expect(elem.selectAll('.modal').size()).to.equal(1);
+ });
+
+ it('shows the title, the reason and the checkbox', () => {
+ mountDialog(context);
+ context.services.plateau.emit('osmlayeroff');
+ expect(elem.selectAll('.modal-section.header h3').text())
+ .to.equal('plateau_conflation.osm_layer_off_title');
+ expect(elem.selectAll('.modal-section.message-text p').text())
+ .to.equal('plateau_conflation.osm_layer_off');
+ expect(elem.selectAll('.plateau-dont-show-again input').size()).to.equal(1);
+ });
+
+ it('does not open the dialog when the user asked not to see it', () => {
+ context.systems.storage.setItem(HIDDEN_KEY, 'true');
+ mountDialog(context);
+ context.services.plateau.emit('osmlayeroff');
+ expect(elem.selectAll('.modal').size()).to.equal(0);
+ });
+
+ it('remembers the choice when the checkbox is ticked', () => {
+ mountDialog(context);
+ context.services.plateau.emit('osmlayeroff');
+ const node = elem.select('.plateau-dont-show-again input').node();
+ node.checked = true;
+ happen.once(node, { type: 'change' });
+ expect(context.systems.storage.getItem(HIDDEN_KEY)).to.equal('true');
+ });
+
+ it('forgets the choice when the checkbox is unticked', () => {
+ mountDialog(context);
+ context.services.plateau.emit('osmlayeroff');
+ const node = elem.select('.plateau-dont-show-again input').node();
+ node.checked = true;
+ happen.once(node, { type: 'change' });
+ node.checked = false;
+ happen.once(node, { type: 'change' });
+ expect(context.systems.storage.getItem(HIDDEN_KEY)).to.be.null;
+ });
+});