# 部品(Web Components)— S2
HTML だけで地図を置くための部品です。**ビルド工程はありません。**
`packages/elements/src/*.js` が素の ESM で、そのまま配られます。
```html
```
手元で動かすなら `pnpm serve` のあと 。
点をまとめる例は 。
## 属性
### ``
| 属性 | 意味 |
|---|---|
| `tiles` | PMTiles の URL(必須) |
| `style-url` | スタイル JSON の URL(必須) |
| `center` | `経度,緯度`(GeoJSON と同じ並び)。既定は大阪 |
| `zoom` | 0〜24。既定 12 |
| `hash` | 付けると URL の `#zoom/lat/lon` で場所を持つ |
| `3d` | 付けると建物を押し出す。**出したあとでも付け外しできます**(下記) |
| `pitch` | 傾き 0〜85。既定 0、`3d` を付けたときだけ 45。**`hash` と併用しても効きます**(下記) |
| `accent` | **サイトのテーマカラー 1 色**。高速道路・駅の丸の縁・目印/まとまり/POI の既定色に当たる(下記) |
| `palette-url` | 役割ごとに色を指定した JSON の URL。`accent` より優先する(下記) |
| `cooperative` | 付けると 1 本指はページ送り、2 本指だけ地図が取る。**1 ページに何枚も置くときに要る** |
| `lang` | ラベルを出す言語(`en` / `zh-Hant` / `ko` …)。**地域ごとにスタイルを増やさないための仕組み**(下記) |
| `ideograph-fonts` | 漢字かなを描くフォント。既定は日本語向け。**中国語・韓国語の地図では差し替える** |
| `station-icon` | 駅の印を、置く側の SVG に差し替える(**既定は円**。下記) |
| `station-icon-size` | その記号の大きさ。既定は `1` |
| `no-coverage` | 付けると、**入っていない場所の表示を止める**(下記)。惑星ビルドを丸ごと配っている人向け |
| `locate` | 付けると**現在地のボタン**を出す。向きと精度の円も出る(下記)。**付けないかぎり位置情報は要求しません** |
| `fullscreen` | 付けると**全画面ボタン**を出す。**付けないかぎり出ません**(下記) |
### `fullscreen` — 地図を全画面にする
```html
```
**頁の一部に置かれた地図は、周りの文字より小さいことが多い**です。
広げる手段が無いと、**指で動かすたびに周りの文字が邪魔をします**。
**勝手には出しません**。`locate` と同じで、置く側が決めます
(地図を並べた頁では、1 枚ずつにボタンが並ぶと騒がしくなります)。
### `locate` — 通信が無くても、現在地は出る
```html
```
**GPS は通信が無くても動きます**。測位は衛星からで、通信ではありません。
つまり**タイルが手元にあれば、停電でも輻輳でも「いま自分がどこで、どっちを向いているか」が分かります。**
欠けていた唯一のピースは、**地図のほうがネット越しだった**ことだけでした。
| 設定 | 値 | なぜ |
| --- | --- | --- |
| `enableHighAccuracy` | `true` | **衛星を優先する**。Wi-Fi や基地局から位置を出す方式は、**まさに通信が無い場所で効かない** |
| `timeout` | **入れない** | 通信が無いと初回の測位に**数十秒**かかる(普段は軌道情報を通信で先取り=A-GPS)。切ると**効いてほしい場面で自分から諦める** |
| `showUserHeading` | `true` | **避難では「どっちへ走るか」が要る**。点だけでは向きが分からない |
| `trackUserLocation` | `true` | 止まった点だけでは、歩いている人に使えない |
**押すまで測位は始まりません**。開いた瞬間に許可を求める画面は、それだけで閉じられます。
**事前に許可を取っておいてください**。災害時に初めて許可ダイアログを出すのでは遅い、
というのは**タイルとまったく同じ構造**です。普段使ってもらうことが、そのまま備えになります。
> **位置情報はどこへも送られません**。画面に出すだけで、保存も送信もしていません。
> 位置情報 API は HTTPS でのみ動きます(`file://` では出ません)。
### `no-coverage` — 入っていない場所の見せ方
1 つの `.pmtiles` は地球の一部しか持ちません。**その外へ動かしたとき、
何も出ないのは正常な状態**ですが、**画面からは「読み込み中」とも「壊れた」とも
区別がつきません**。
既定では、こう見せます。
1. **取りに行って空だったタイルの場所を塗る**(常時)
2. **何も描けていないときだけ、一行を出す** — 「このタイルに入っていない範囲です。
切り出す範囲を広げると出ます。」
**宣言された範囲は使っていません。** PMTiles のヘッダは範囲を名乗りますが、
それは入っている範囲とは限りません。実測(2026-09-24・`pmtiles show`):
demo.pmtiles の bounds: (122.93, 20.42) - (153.99, 45.56) ← 日本全体と名乗る
実際は **z0〜9 が全国の俯瞰・z10〜15 が大阪だけ**という二段構えで、
**四角 1 つでは表せません**。宣言を信じると、入っていない場所を
「入っている」と描くことになります。代わりに**取りに行った結果**を見ています。
実物で確認(2026-09-24・`demo.pmtiles`): 岐阜の上 z12 で**欠けたタイル 6 枚を検出し、
一行が出た**。大阪の東の縁 z11 では**描画地物 715 件**が出ているので一行は出さず、
**境目だけが面として見える**。
止めるには `no-coverage` を付けます。
```html
```
### `lang` — ラベルの言語
```html
```
読み込んだあとに `text-field` を差し替えます。**スタイルは 6 枚のままです。**
地域 × 配色でスタイルを持つと **6 枚が 24 枚 48 枚と増えて破綻する**ので、色と同じく
「読み込んだあとに当てる」形にしてあります。
**`name` への落とし先は残ります**。その言語の名前が無い地物は、元の名前のまま出ます。
**消えません。**
> **日本の外では `lang` を必ず付けてください。**
> 配っている 6 枚のスタイルは `name:ja` を優先します。付け忘れると、
> **`name:ja` を持つ地物だけ日本語**になり、残りは現地語という**混在**になります。
>
> 実例(2026-09-22・実物で確認): ホーチミンを `lang` 無しで開くと、
> 市の名前が **「ホーチミン市」** と日本語で出て、周りの水路はベトナム語のままでした。
> `lang="vi"` を付けると揃います。
#### 使えるキーは、タイルを開いて数えてください
上流が持っているキーは地域によって違います。大阪のタイルで数えた実測(2026-09-21):
| キー | 件数 |
| --- | --- |
| `name:en` | 508 |
| `name:zh-Hant` | 20 |
| `name:zh-Hans` | 20 |
| `name:ko` | 9 |
| `name:vi` | 2 |
**`name:zh` は存在しません**。`zh-Hans` / `zh-Hant` に分かれています。推測で書くと当たりません。
```bash
pnpm tile:inspect -- dist/tiles/demo.pmtiles 12 3589 1626 --layer=places --list
```
**大阪で `lang="zh-Hant"` にしても、見た目はほとんど変わりません**(20 件しか無いため)。
**台北のタイルでは効きます**(`name:zh-Hant` が 215 件・2026-09-22 に実物で確認。
`docs/screenshots/2026-09-22-taipei-lang.jpg`)。
**件数が少ない = 効かない、ではありません**。ベトナムは `name:vi` が 5 件しかありませんが、
`name` 自体がベトナム語なので `lang="vi"` で正しく出ます。
**当たらなければ例外を投げます**。言語を指定したのに元のままの地図を黙って出すと、
「指定したのに変わらない」という気づきにくい壊れ方になります。
### `station-icon` — 駅の印を差し替える
```html
```
**既定は、スタイルの中の円です**(`station-dot`)。書かなければ今までどおりで、
**スタイルを MapLibre へそのまま渡している人にも駅が出ます**。
`station-icon` を書いた地図だけ、その SVG が駅の位置に出ます。
出す条件(ズーム・絞り込み)は円とまったく同じです。
- **円の層は消えません**。描かれなくなるだけで、`palette-url` の `station` /
`station-fill` の宛先は残ります(色を渡したのに当たらない、が起きない)
- **読めなければ円のまま**です。地図は出ます(警告だけがコンソールに出ます)
- 別オリジンの SVG には **CORS が要ります**(画素を読み戻すため)
- `javascript:` のような URL は弾きます
> **なぜスプライトではないのか**: MapLibre は `sprite` の相対 URL を認めません
> (`Invalid sprite URL ... must be absolute`)。スタイルに絶対 URL を焼くと、
> **自前で配っているのに配信元がこちらのドメインに固定**されます(D-001)。
> だから既定は資産の要らない円にし、差し替えだけを実行時に足しています。
### `ideograph-fonts` — 漢字かなのフォント
CJK のグリフは配っていないので、漢字かなは**閲覧側のフォント**で描きます。
既定は日本語向け(`Noto Sans JP` ほか)で、**中国語や韓国語の地図では字形が合いません**
(同じ符号でも国ごとに字体が違う)。
```html
```
**空文字では上書きしません**(空のフォント指定は字を消します)。
### `accent` / `palette-url` — サイトの色を当てる
**ほとんどの導入先には、そのサイトのテーマカラーがあります。**
180 行のスタイル JSON を書かせないために、色だけを渡せるようにしてあります。
```html
```
`accent` の 1 色が当たるのは **2 か所だけ**です。
| 当たる | 何 |
| --- | --- |
| `highway` | 高速道路の線。**画面で一番目に入る線** |
| `station` | 駅の丸の縁 |
加えて、`` / `` / `` の**色を指定しなかったとき**の既定が
この色になります。ポップアップの箱と文字は、**土台のスタイルから借ります**
(明るいスタイルなら明るい箱)。
**地図全体をテーマカラーで塗りません**。塗ると陸・水・建物・道の区別が付かなくなり、
地図として読めなくなります。**もっと広く変えたいときは `palette-url`** を使ってください。
```html
```
```json
{
"background": "#FFFFFF",
"earth": "#FFFFFF",
"water": "#DCE9F5",
"buildings": "#EFF2F6",
"road-minor": "#FFFFFF",
"road-medium": "#FFFFFF",
"road-major": "#F4F6F9",
"highway": "#0A5FFF",
"highway-casing": "#0842B5",
"label-city": "#10172A",
"halo": "#FFFFFF"
}
```
役割は 24 個あります。**書いたものだけが当たり、書かなかったところは土台のまま**です。
`background` `earth` `landcover` `green` `built` `paved` `water` `waterway` `buildings`
`path` `road-minor` `road-medium` `road-major` `highway-casing` `highway` `rail` `boundary`
`station` `station-fill` `label-city` `label-station` `label-neighbourhood` `label-water` `halo`
`accent` と `palette-url` を両方書いたときは `palette-url` が勝ちます(細かいほうを優先)。
**当たらなかったら例外を投げます。** 色を渡したのに 1 つも当たらない地図を黙って出すと、
「指定したのに変わらない」という気づきにくい壊れ方になります。
自前のスタイルに当てるときは、`styles/README.md` のレイヤ id を保ってください。
**こちらは色を作りません**(baseline §11 / D-002)。1 色から陸・水・道の明度を機械的に
振って土台ごと生成する案は、**D-002 が採らなかった案**なので入れていません。
必要なら提案として出します。
#### いま出来ないこと
- **読み込んだあとに `style-url` / `accent` を書き換えても切り替わりません。**
生成時に 1 回だけ読みます。切り替えたいときは要素を置き直してください
(`apps/demo/brand.html` がそうしています)
### 地図の時点(帰属表示に自動で出ます)
```
© OpenStreetMap contributors · 地図データ: 2026-09 時点
```
**配った `.pmtiles` は凍った成果物です**。上流は毎日更新されますが、
手元のファイルは切った日のままで、置き換えるまで古びていきます。
**画面に何も出ないと、8 か月前の地図を配り続けても誰も気づけません。**
Protomaps / planetiler で作られたタイルには、**取り込んだ OSM の時刻**が入っています。
それを読んで、`© OpenStreetMap contributors` の隣へ並べます。
**設定は要りません**。自分で切ったタイルにも同じ値が入ります。
- **OSM の取り込み時刻を優先します**。組み立て時刻とはずれます
(実測: 取り込み 2026-09 / 組み立て 2026-03)。
**組み立て時刻を「地図の時点」と名乗りません**
- **月までしか出しません**。日まで出すと「今日の地図」に見えるためです
- 時点が入っていないタイル(planetiler 以外)では**何も出ません**
- 読めなくても、遅くても、**地図は出ます**(3 秒で諦めます)
古びていないかは、配信先に対して数えられます。
```bash
pnpm tiles:check-age -- https://example.com/tiles/japan.pmtiles --days=90
# 地図データの時点: 2026-09-15T04:00:00Z(15 日前)
# しきい値 90 日以内です
# mmj:stale=false
```
配信先を HTTP Range で読むだけなので、**go-pmtiles は要りません**。
最後の行は**機械が読む印**です(`mmj:stale=true` / `false` / `unknown`)。
CI から使うときは、**この行を見てください**。人が読む文面は直ることがあります。
**古いこと自体では落ちません**——切り直すかどうかは人の判断です。
**時点を読めなかったときだけ落ちます**(終了コード 1)。
読めていない検査は、通った検査と区別がつかないからです。
### `3d` — 建物の押し出し
```html
```
**タイルは 2D のときと同じものです**。足すデータも、追加の取得もありません。
| | リクエスト | 転送量 |
|---|---|---|
| 2D | 17 | 735,015 B |
| 3D | 17 | 735,015 B |
(梅田 z16 / pitch 60 の同一視点で実測・2026-09-20)
変わるのは描き方だけなので、**スタイルの決定ではなく、置く側の切り替え**にしてあります。
手書きスタイル(`styles/modern-dark.json`)は書き換えません。読み込んだ後のオブジェクトへ
押し出しレイヤを 1 枚足すだけです。
知っておくべきことが 3 つあります。
- **高さを持つ建物だけが立ちます**。実測で **3 割ほどが `height` を持ちません**
(梅田 12,166/17,651・難波 20,383/29,435)。持たないものは 2D のまま平らに残ります。
**既定値で埋めていません**。データに無い高さを、あるように見せないためです
- **色は決めていません**。押し出す面の色は、スタイルの 2D 建物レイヤが持っている
`fill-color` をそのまま読みます。ここで新しい色を作ると、人が決めていない配色が
既成事実になります
- **陰影は MapLibre の既定のままです**。面ごとの明暗を決めていません。
`DESIGN.md` に 3D の規定が無いため、**仮置き**です
**`3d` は、出したあとでも付け外しできます。**
```js
document.getElementById('map').toggleAttribute('3d');
```
知らない街を歩くときは「その角のビルの形」が効きますが、**ずっと立っていると
上から道をたどるのが読みにくい**ので、切り替えられるようにしてあります
(デモは「建物を立てる」と「経路と案内」の左上にボタンがあります)。
- **2D の建物は消えません**。3D のときも平らなレイヤは残ります
(高さを持たない 3 割がそこに出ます)
- **経路や目印より下に入ります**。立てても案内の線が建物に隠れません
- **足りないぶんだけ地図を動かします**。押し出しは寄って(z14 未満では 1 棟も出ない)・
傾けて初めて見えるので、真上から見ているときだけ 45 度へ倒します。
**すでに寄っている/傾けている人からは、その値を奪いません**
イベント: `mmj-ready`(`detail.map` に MapLibre の Map)/ `mmj-error`。
### ``
| 属性 | 意味 |
|---|---|
| `lnglat` | `経度,緯度`(必須) |
| `popup` | 押したときに出す文字 |
| `image` | 吹き出しに載せる写真の URL(**http / https と相対パスだけ**) |
| `image-alt` | 写真の代替テキスト。省くと `popup` の文字を使う |
| `popup-open` | 最初から開いておく |
| `color` | 目印の色 |
### ``
点が多いときに、まとめて描きます。**まとめる計算は MapLibre の GeoJSON source が
持っている**ので、この部品は 3 枚のレイヤを排他の filter で置くだけです(D-011)。
```html
```
| 属性 | 意味 |
| --- | --- |
| `src` | 点の GeoJSON の URL(必須) |
| `radius` | まとめる距離(px)。既定 50 |
| `max-zoom` | ここより寄ると、まとめずに 1 点ずつ出す。既定 14 |
| `color` | まとまりの色。既定は MapLibre の Marker と同じ `#3FB1CE` |
| `text-color` | 件数の文字色と、丸の縁の色。既定 `#111418` |
| `point-color` | ばらの点の色 |
| `layer-id` | source / layer の名前。既定は自動で振る(同じページに複数置ける) |
- **まとまりを押すと、それが解ける倍率まで寄ります。**
- **ばらの点を押すと `mmj-cluster-point` が飛びます**(`detail.properties` /
`detail.lngLat`)。ポップアップを出すかどうかは**使う側が決めます**。
ここで決めると厚くなるので、部品は中身を渡すところで止めています。
- **件数は色ではなく大きさで表しています**。色の段階は配色を決める行為で、
凡例が無いまま出すと読む人ごとに違う意味に読まれます(baseline §11 / D-011)。
既定の 2 色は**新しく作った色ではなく**、MapLibre の既定値と
`styles/modern-dark.json` に既にある値です。**DESIGN.md が埋まるまでの仮置きです。**
### `` — 経路と案内の吹き出し
```html
```
| 属性 | 意味 |
| --- | --- |
| `src` | 経路の GeoJSON の URL(**必須**) |
| `fit` | 付けると、経路ぜんぶが入るところまで寄せる |
| `color` | 線の色。既定は ``、無ければ `#3FB1CE` |
| `casing-color` | 縁取りの色。既定は地図の地色 |
| `width` | 線の太さ(px)。既定 6。縁取りは +4 |
| `step-key` | 案内文を読む属性名。既定 `instruction`(媒体ごとに違うため) |
| `step-color` | 案内の目印の色。既定は `` |
| `layer-id` | source / layer の名前。既定は自動(同じページに複数置ける) |
渡す GeoJSON はこの形です。
```json
{
"type": "FeatureCollection",
"features": [
{ "type": "Feature", "properties": {},
"geometry": { "type": "LineString", "coordinates": [[135.49,34.70],[135.50,34.69]] } },
{ "type": "Feature", "properties": { "instruction": "コンビニを左折" },
"geometry": { "type": "Point", "coordinates": [135.495,34.695] } },
{ "type": "Feature", "properties": { "instruction": "橋を渡って右手", "image": "./corner.jpg" },
"geometry": { "type": "Point", "coordinates": [135.50,34.69] } }
]
}
```
**後から足せます**。案内の点だけ増やした GeoJSON を渡せば、そのぶんだけ吹き出しが増えます。
AI に経路と案内文を作らせて、この形で渡す使い方を想定しています。
#### `image` を持つ点は、ピンの丸の中にその写真が出ます
**押す前に、写真があることが分かるようにするためです**。開いてみるまで分からないなら、
**写真は無いのと同じ**になります(2026-10-09 に「ピンに画像があるとわからない」という
指摘を受けて入れました)。
- 小さいので**何が写っているかまでは分かりません**。それでよく、
**「何か写真がある」が伝われば目的を果たします**
- `image` を持たない点は**いままでどおりの目印**です。見た目が違うこと自体が合図になります
- **安全でない URL は写真として扱いません**(`http` / `https` / 相対パスだけ。
吹き出しの写真と同じ判定です)。その場合も点は出ます
- 写真は `
` と同じ読み込み方なので、**別オリジンでも CORS の設定は要りません**
置き終わると `mmj-route-ready` が飛びます(`detail.steps` が案内の件数)。
**0 件でも気づけるように**、件数を画面へ出すことを勧めます。
#### **経路は計算しません**
計算には道路グラフとルーティングエンジンが要り、それは D-003(地図サーバーを立てない)の
外側です。**作るのは外**(ルーティング API でも、AI でも)で、ここは描くだけです。
「データを持たない。表現を持つ」の形そのものです。
### 吹き出しに写真を載せる
`` と `` の案内、どちらにも載ります。
```html
```
| 属性 | 意味 |
| --- | --- |
| `image` | 写真の URL。**http / https と相対パスだけ** |
| `image-alt` | 代替テキスト。省くと `popup` の文字を使う |
**`setHTML` を使っていません**。媒体が登録した文字列をそのまま HTML として実行すると、
`
` の 1 行で、その地図を置いたページ全体が乗っ取られます。
`createElement` と `textContent` だけで組むので、**文字が HTML として解釈される経路が
そもそも存在しません**(baseline §21)。
同じ理由で、`javascript:` と `data:` の画像は**黙って落とします**。
`data:` は「画像に見せかけた SVG」からスクリプトが走る経路があるためです。
### ``
**利用者が自分で持っている POI** を、ベース地図の上に重ねます。
ベース地図(Protomaps / OSM)には触りません。**MMJ はデータを持ちません**(D-001)。
```html
```
| 属性 | 意味 |
| --- | --- |
| `src` | 点の GeoJSON の URL(必須) |
| `label-key` | 名前を取る属性名。既定 `name`(媒体ごとに `title` / `shop_name` と違うため) |
| `min-zoom` | ここから出す。既定 13。**件数を知っているのは持ち込む側だけ**なので既定を当てにしない |
| `attribution` | **そのデータの出典と時点**。`© OpenStreetMap contributors` の隣に出る(下記) |
| `color` / `text-color` | 点と名前の色。既定は `` と同じ扱い(新しい色を作っていない) |
| `layer-id` | source / layer の名前。既定は自動 |
| `card-*` | 押したときに出すカードの中身。**属性名を渡すだけ**(下記) |
#### `attribution` — 出典は、地図の中に書く
```html
```
**地図は単体で持ち歩かれます**。オフラインで端末に入ったあと、配布元のページは付いて来ません。
**出典を地図の外に書いても、読む人には届きません。**
とくに避難所の指定のように**間違っていると人が危ないデータ**を重ねるときは、
**どこの・いつ時点のものかが、その場で読めること**が要ります。
> **MMJ は渡されたデータを描くだけで、正しさはデータの出どころが持ちます。**
> だからこそ、**出どころを名乗れる場所**を部品が用意しています。
**時点まで書いてください**。指定は変わります。`(2026 年 9 月時点)` のような
一文が無いと、読んだ人はいつのものか確かめようがありません。
渡さなければ何も出ません(**空の出典欄を出して、出どころを騙らない**ため)。
#### `card-*` — 押したら、地図から離れずに店を見る
```html
```
| 属性 | 何を渡すか |
| --- | --- |
| `card-id` | **その点を一意に指す値**が入っている属性名。次/前の送りと URL の復元に要る |
| `card-title` | 名前が入っている属性名 |
| `card-images` | 写真の URL が入っている属性名。**配列でも、`,` か改行で区切った文字列でも受けます** |
| `card-body` | 説明文が入っている属性名 |
| `card-image-credit` | **撮影者の表記**が入っている属性名。写真の直下に小さく 1 行出ます(下記) |
| `card-image-credit-href` | その名前に張るリンクの URL が入っている属性名。**任意**(無ければ名前だけ出ます) |
| `card-rating` / `card-rating-count` | 評価と件数。`4.3 / 5(128 件)` の形で出ます |
| `card-href` | 飛び先の URL が入っている属性名。**`http` / `https` / 相対 / `mailto:` / `tel:` だけ**通します |
| `card-href-label` | そのリンクに出す文字(`card-href` とちがい、**値そのもの**) |
| `card-tabs` | タブ。`"見出し:属性名,…"` の並び(下記) |
| `card-hours` | **曜日ごとの営業時間**が入っている属性名(下記) |
| `card-reviews` | **口コミ本文**が入っている属性名(下記) |
| `card-links` | SNS などへのリンク。`"見出し:属性名[:アイコンのURL],…"`(下記) |
| `card-rich` | **本文を HTML として読む項目**の名前。既定は読みません(下記) |
| `card-live` | **時間で変わる中身**を、開いたときに取りに行く。`"見出し:URLの型"`(下記) |
| `list` | 一覧の入れ物の CSS セレクタ。地図と一覧を繋ぐ(下記) |
| `icon-key` | 業種などの名前が入っている属性(下記) |
| `icons` | 名前 → SVG の URL。`ramen:./icons/ramen.svg,…`(下記) |
| `pin-image` | **ピンの中に出す写真**の URL が入っている属性名(下記) |
| `rank-key` | **並び順**を決める値が入っている属性名(下記) |
| `rank-order` | 値の並び。`platinum,gold,silver,bronze`(左が上) |
| `pin-color-key` | **ピンの色**を決める値が入っている属性名(下記) |
| `pin-colors` | 値 → 色。`ramen:#E4572E,cafe:#C9A227`(**色は導入側が指定**) |
**渡すのは属性名で、値ではありません**。500 店のカードを手で書かないためです。
**媒体ごとにキーの名前が違う**(`name` / `title` / `shop_name`)ので、MMJ は推測しません。
`card-*` を 1 つも書かなければカードは出ず、今までどおり `mmj-poi-click` だけが飛びます。
**写真が 2 枚以上あると、横に流れる帯になります。**
**指で払えない画面(PC)には、左右に送りボタンが出ます**(触る画面では出ません——
指のほうが速く、ボタンは写真を隠すため)。縦に積むと、星も説明文もリンクも
画面の外へ出るためです。**次の 1 枚の端が覗く**幅にしてあるので、続きがあることが分かります。
帯は `Tab` で触れて矢印キーで送れ、読み上げには `写真 3 枚` と出ます。
#### `card-image-credit` — 写真に添える撮影者の表記
```html
```
写真の提供元が「**写真に添えて撮影者名を表示すること**」を条件にしていることがあります。
これは体裁ではなく**使用条件**なので、`card-body` に混ぜる(説明文と区別が付きません)、
`card-tabs` に入れる(**開かないと見えません**)では満たせません。
**写真のすぐ下に、小さく 1 行**出ます。
- **リンクが無ければ、名前だけ出ます。行は消えません**。`card-image-credit-href` が
空(`null` / 空文字)のときは、空のリンクを出さず、**行ごと落とすこともしません**——
中身の無いタブを落とすのとは逆の扱いです。**名前を出すこと自体が条件**なので、
リンクの有無で消えてはいけません
- URL は `card-href` と同じ判定を通します。**`http` / `https` / 相対 / `mailto:` / `tel:` だけ**で、
`javascript:` は黙って落とします(**そのときも名前は残ります**)
- **写真が 1 枚も出ていない地物では、この行も出ません**(添える先がありません)。
渡した写真が全部落ちた(危ない URL だけだった)ときも同じです
- 文字は小さくしますが、**読めなくはしません**。帰属表示と同じ扱いです
(`LICENSES.md` / D-007 / D-028)
> ### 写真ごとに撮影者が違う場合は、この口では足りません
>
> `card-image-credit` は**その地物に 1 つ**です。`card-images` は複数枚を受けますが、
> **ギャラリー全体に 1 行**だけ出て、**写真ごとには割り当てません**。
>
> 撮影者を枚数に合わせて推測で割り振ると、**別の写真に別人の名前が付きます**。
> それは表記が無いより悪い壊れ方なので、やりません。
>
> 枚数と並んだ撮影者を渡したい場合は、**形を決めるところから必要です**
> (データ側が「写真の数と同じ長さの並び」を持つ前提になります)。
> その要望が出てきたときに、改めて決めます。
#### `card-hours` — 曜日ごとの営業時間
```html
```
```json
{
"mon": ["10:00-18:00"],
"wed": [],
"sat": ["11:00-14:00", "17:00-21:00"]
}
```
「営業時間」のタブが増え、**月から日まで 7 行**出ます。
**書かれていない日と、空の日は同じ「休み**」です(勝手に営業していることにしません)。
**今日の行だけ強く**出ます。
> ### 「営業中」とは書きません
>
> **MMJ は判断しません**。いま開いているかどうかは、**時差・祝日・臨時休業・
> ラストオーダー**を知らないと言えません。知らないまま「営業中」と出すのは嘘で、
> **その嘘は、閉まった店の前に人を立たせます**。
>
> 出すのは**書かれている時間そのもの**と、**今日がどの行か**まで。
> 「いま開いているか」を出したいなら、**それは置く側が自分の責任で計算してください**
> (`card-live` で「本日 17 時まで」のように渡すこともできます)。
#### `card-reviews` — 口コミ本文
```html
```
```json
[{ "text": "二階の座敷が静かです", "rating": 5, "name": "きの", "date": "2026-09" }]
```
「口コミ(3)」のタブが増えます。文字列の並び(`["静かでよい", "珈琲が濃い"]`)や、
改行で区切った 1 つの文字列でも受けます。
- **本文が主役**。星・名前・日付は**あるものだけ**添えます
- **本文が無いものは出しません**(名前と星だけの口コミは読めない)
- **MMJ は要約しません**。書いた人の言葉のまま出します——
要約した時点で、**書いた人が本当に困っていた所が落ちます**
> **星の件数とは別の属性にしてください**。`card-rating-count` と同じ名前に入れると、
> **件数が消えます**(実際にやりました・2026-09-29。`4.3 / 5(128 件)` が
> `4.3 / 5` になった)。
#### `card-live` — 時間で変わる中身(クーポン・シフト・今日の品切れ)
```html
```
カードを開いたときに `./data/live/<その店の id>.json` を取りに行き、
取れたら**タブを 1 枚増やします**。返す形は `"文字列"` か `{"text": "文字列"}`。
**GeoJSON に書かないでください**。クーポンやシフトは時間で変わるので、
地図と一緒に持ち歩かれると**先月のクーポンが今日の顔で出ます**。
| | |
| --- | --- |
| 掴まない | `cache: "no-store"`。**掴むとこの機能の目的そのものが消えます** |
| 取れなくても | **カードは出ます**。そのタブだけが出ません |
| 404 | **異常ではありません**。「この店には、いまの情報が無い」として静かに扱います |
| 送って回ったとき | **遅れて返った古い応答は捨てます**(別の店の情報を今の店の顔で出さない) |
| `{id}` が無い型 | 使いません(**全店で同じものを取りに行く**壊れ方になるため) |
> ### これを使うと、「どの店を開いたか」が配信元に伝わります
>
> **MMJ の部品はどこへも送りません**が、**取りに行く先は使う側のサーバー**なので、
> そこにはアクセスの記録が残ります。**使うかどうかは置く側の判断です**
> (属性を書かなければ、取りに行きません)。
>
> 時間で変わらないもの(品書き・営業時間)は `card-tabs` で GeoJSON に持たせてください。
> **そちらは掴めるので、オフラインでも出ます。**
#### `card-rich` — 本文に文中リンクと強調を書く
```html
```
**既定では、本文は文字として出ます**。`` と書いても、そういう文字が出るだけです。
`card-rich` で**項目を名指ししたときだけ**、その項目を HTML として読みます。
```
二階は予約席です。席を予約すると、窓際を確保できます。
```
| 許すタグ | `a` `b` `br` `em` `i` `li` `ol` `p` `strong` `ul` |
| --- | --- |
| 許す属性 | **`a` の `href` だけ**。`target` と `rel` はこちらが付けます |
| 落とすもの | 上に無いタグ、**すべてのイベント属性**(`onerror` など)、`style`、安全でない `href` |
| 中身ごと捨てるタグ | `script` `style` `template` `noscript` `iframe` `object` `embed` |
**タグが落ちても、中の文字は残します**(知らないタグで文章ごと消さないため)。
`href` は他のリンクと同じ判定を通り、`http` / `https` / 相対 / `mailto:` / `tel:` だけが通ります。
> **なぜ既定で閉じているのか。**
>
> 置く側が自分でデータを書くなら、HTML を許しても**自分のページに自分で書くのと同じ**で安全です。
> ただし**店舗が CMS で紹介文を書く**使い方や、`card-live` が実行時に取りに行く JSON では、
> 書く人は置く側ではありません。1 件が `
` を書くだけで、
> **その地図を置いた全ページ**が乗っ取られます。
>
> **部品はどちらか判定できません**。だから既定を安全側に置き、置く側が名指ししたときだけ開きます。
> **他人が書く文字列を入れる項目には、`card-rich` を指定しないでください。**
#### `card-links` — SNS や電話へのリンク
```html
```
**MMJ はブランドのアイコンを配りません**。商標なので、こちらが持つと
**使う側が意図しない形で配る**ことになります。**アイコンは置く側が渡してください**
(3 つ目に URL を書く)。省くと文字だけのリンクになります。
- **中身のあるものだけ出ます**。SNS をやっていない店にリンクは出ません
- **URL は確かめます**。`http` / `https` / 相対 / `mailto:` / `tel:` 以外は落とします
(`javascript:` は押した瞬間に走るので、**アイコンの URL も同じ目で見ます**)
- 外部へ出るリンクは `target="_blank"` + `rel="noopener noreferrer"`
#### `card-tabs` — 品書き・クーポン・営業時間を 1 枚に入れる
```html
```
**長文の下に足していくと、開いた人は下まで読みません**。タブなら 1 枚のカードに並びます。
- **`見出し:属性名` の組**を並べます。**見出しだけが値**——媒体の属性名を
そのまま出すと、画面に `menu` と出てしまいます
- **最初のタブは「概要**」(星・長文・リンク)。いままでどおり最初に見えます
- **題と写真はタブの外**に残ります。切り替えた先が何の店か分からなくならないように
- **中身が空のタブは出ません**。クーポンのある店と無い店が混ざるのが普通なので、
**押しても何も出ないタブを並べない**
- **改行は残します**(`white-space: pre-line`)。品書きは行で分かれているのが中身そのもの
- タブは `Tab` で入り、**左右キーで切り替え**られます(`role="tablist"` の作法)
- **中身は消さずに隠します**。作り直すと、そのタブで読んでいた位置が毎回戻ります
- **1 枚しか中身が無ければ、タブの帯は出ません**
> **時間で変わるものを、地図と一緒に配らないでください**。クーポンやシフトは
> GeoJSON に書いた時点の値です。**古い値が地図と一緒に持ち歩かれる**ことを
> 前提に、期限を本文へ書くか、開いたときに取りに行く形にしてください。
#### `card-id` — 隣の店へ送る・URL で戻す
`card-id` を渡すと、次の 2 つが増えます。**渡さなければどちらも出ません。**
1. **見出しバーに [‹] [›] が出ます**。カードを開いたまま隣の点へ送れます。
**閉じて開き直しません**ので、最大化したまま・写真をめくった位置もそのままで送れます。
端まで行ったら反対側へ回ります(端でボタンを消すと**押す場所が動く**ため)
2. **開いている点が URL に残ります**(`?shop=`)。その URL を開くと、
**その点まで地図が寄って、カードが開いた状態で始まります**
```html
```
> **`#` ではなく `?` に書きます**。`` を付けると MapLibre が
> `#15/34.70/135.49` を書きますが、**その実装は自分の形以外を捨てます**
> (5.24.0 と 6.12.0 で確認)。`#shop=` に置くと、**地図を少し動かしただけで消えます**。
> 消えたことは画面に出ないので、**共有して初めて壊れているのが分かります**。
**一覧はもう一度読みます**。送るにも復元するにも順番の付いた一覧が要り、
`querySourceFeatures` は**画面に出ているぶんしか返さず、順番も保証されません**。
同じ URL なのでブラウザの控えが効きます(地図がすでに取っています)。
**履歴は積みません**(`replaceState`)。押して回るたびに履歴が増えると、
**頁から出るのに戻るボタンを何度も押す**ことになります。
見出しバーの **[⤢] で広げられます**([⤡] で戻る、`Esc` は 1 段だけ戻る)。
**広げても地図は消えません**——広い画面では横幅が伸びるだけで、地図は右に残ります。
**広い画面では、地図の左から出る面になります**(Google マップの店舗欄と同じ形)。
吹き出しは点に貼り付くぶん幅が取れず、**写真も口コミも読めません**
(PC の吹き出しは狭く、Google マップのように横から大きく出してほしい、
という指摘・2026-09-29)。
**この面では写真を敷き詰めます**——1 枚目を大きく、残りを並べる形で、
**横スライダーは出しません**(マウスでは横に払えないため)。
#### `icon-key` / `icons` — 点を自前のアイコンにする
```html
```
```json
{ "properties": { "shop_name": "らーめん 汀", "category": "ramen" } }
```
点が全部同じ丸だと、**地図を見ても何屋かが分かりません**。
うどん・ラーメン・ケーキ・コーヒーが一目で分かれば、**開く前に選べます**。
- **絵は同梱しません**。業種の分け方も絵の趣味も置く側のものです(MMJ はデータを持ちません)
- **アイコンが無い点は、いままでの丸のまま**。分類の無い点も、綴りの違う点も、静かに丸になります
- **1 枚読めなくても地図は出ます**。読めなかった名前はコンソールに出します
- **絵はピンの形に入ります**(写真のピンと同じ形)。先がその地点を指します。
片方だけ平らな丸だと、**同じ地図に 2 種類の目印が並んで読み分けられません**
- 絵は丸の中に **24px** で描きます。SVG に寸法が無くても出るよう、こちらで決めています
- **重なっても消しません**(名前は消えますが、何屋かが消えると地図の意味が変わるため)
> **SVG は `
` として読みます**。`
` に入れた SVG の中では
> スクリプトが動かないので、持ち込まれた絵から script が走る経路はありません。
> `javascript:` の URL は、吹き出しのリンクと同じ規則で落とします。
**選ばれている点は、ピンを囲む輪が出ます**(ピンより大きい輪なので、絵や写真に隠れません)。
#### `pin-image` — 写真を持つ点を、写真入りのピンにする
```html
```
**押す前に「ここに写真がある」と分かるようにするための口**です。カードを開くまで
分からないなら、**写真は無いのと同じ**になります(2026-10-09 の指摘で入れました)。
- **合図のための別の印は付けません**。丸の中が写真になっていれば、
**小さくて何が写っているか分からなくても「写真がある」は伝わります**
- `card-images` と**同じ属性をそのまま渡せます**。複数枚あれば**先頭の 1 枚**を使います
- **写真を持たない点は、いままでどおり**(`icon-key` の絵か、無ければ丸)。
見た目が違うこと自体が合図になります
- **安全でない URL は写真として扱いません**(`http` / `https` / 相対パスだけ)。
**危ないものが混ざっていたら、その値ごと使いません**——`data:` は中に `,` を含むので、
区切って 1 つずつ見ると後半が「相対パス」に見えてしまうためです
- 選ばれている点は、**ピンを囲む輪**が出ます
> ### 別オリジンの写真には CORS が要ります
>
> ピンは写真を**丸く切ってから地図へ渡す**ので、canvas から画素を読み戻します。
> 配信側に `Access-Control-Allow-Origin` が無いと読めません。
> **そのときは何も出さず、いままでの丸(または分類の絵)のまま**です(穴は開きません)。
>
> 同じオリジンに置いてある写真なら、設定は要りません。
#### `rank-key` / `rank-order` — 媒体のプランで並べ替える
```html
```
```json
{ "properties": { "shop_name": "町家カフェ 灯", "plan": "gold" } }
```
**MMJ は「ゴールド」の意味を知りません**。段の数も名前も媒体ごとに違うので、
受け取るのは「どの属性を見るか」と「どの順に並べるか」だけです。
- **一覧も `‹ ›` の送りも `?shop=` の復元も、同じ順になります**
(どれか 1 つだけ並び替えると、どれを見ているのか分からなくなります)
- **同じ順位の中では、元の順をそのまま保ちます**(安定)。
そうしないと、**同じプランの店が読み込むたびに入れ替わります**
- **並びに無い値と、値を持たない点は末尾**(そこでも元の順のまま)。
綴り違いや新しいプランで、**店が一覧から消えることはありません**
- 指定が無ければ、いままでどおり GeoJSON の順です
- HTML に書いた一覧は**作り直しません**。置く側が書いた要素を並べ替えるだけなので、
検索に出ていた文字は消えません
> ### お金を払った順に並ぶ一覧は、広告です
>
> 日本では 2023-10 から**ステルスマーケティング規制**(景表法の指定告示)があり、
> **広告であることが一般消費者に分かるようにする**必要があります。
>
> **MMJ は表示文言を決めません**(媒体の判断なので、こちらが決めると必ず合いません)。
> **掲載順がプランで決まっているなら、そう分かる表示を出してください。**
#### `pin-color-key` / `pin-colors` — ピンの色を、点ごとに変える
```html
```
既定では、ピンの色は要素の `color`(または ``)で**全部同じ**です。
値ごとに変えたいときは、`icons` と同じ「**値 → 何か**」の対応で渡します。
- **対応に無い値は、いままでの色**(知らない値で点を消しません)
- **写真のピンにも、分類の絵のピンにも効きます**
- **色は導入側が指定します**(MMJ は色を作りません)。
**16 進だけ**を受けます——`red` のような名前は捨てます
(canvas は読めない色を黙って透明にするので、通すと**ピンが消えた理由が分からない**)
- **色だけに意味を持たせないでください**。色で業種を表すなら、
`icon-key` の絵や一覧の文字でも分かるようにしてください
**件数が多くても大丈夫です**。写真は**地図が実際に描こうとしたぶんだけ**作ります
(画面の外も、ズームで間引かれた点も作りません)。同時に持つのは **96 枚**までで、
超えたら古いものから捨て、また描くときに作り直します。
#### `list` — 地図と並ぶ一覧
```html
```
カードは送りボタンで 1 件ずつしか回れません。3 件なら足りますが、
**20 件を「探す」ことはできません**。一覧はそこを埋めます。
**一覧は HTML に書いてください**。そこに文字があるから、
- **検索に出ます**(店名も評価も、いまは HTML に 1 文字もありません)
- **JS を切っても読めます**
- **Tab で届きます**(点は canvas の上の描画なので、キーボードでは 1 件も触れません)
`data-shop` の値が `card-id` の指す値と一致した項目に、
**開いている店の印**(`aria-current="true"`)が付きます。見た目は置く側が決めます。
押すと**頁を読み込み直さずに**カードが開き、地図がその点まで寄ります。
`href` は本物のリンクなので、**Ctrl+クリックで新しいタブ**にも、
**JS を切った状態**にも、そのまま効きます。
> **入れ物が空なら、部品が一覧を作ります**。探すのとキーボードには効きますが、
> **検索には効きません**(読み込んだ後に生えるため)。
> 検索に効かせたいなら、**HTML に書いてください**——
> 実際のサイトでは CMS や静的サイト生成が、地図の GeoJSON と一覧を同じ元から出します
> (MMJ はデータを持ちません)。
**矢印で送ったとき、どの点の話かが分かります。**
- 開いている点だけ**大きくなります**(別のレイヤなので、**密集していても隣の点に隠れません**)
- 点の上に**名前だけの小さな吹き出し**が残ります。面には読むものがあり、こちらは
「ここ」を示すためだけのものです
- 選ばれている点の名前は、**地図のラベルからは消します**(同じ文字が 2 つ並ぶため)
これには `card-id` が要ります(点を id で引けないと印を立てられません)。
**狭い画面では、下から出る面になります**。地図は上に残り、
**押した点が面に隠れないよう、地図のほうが動きます**(Google マップ・Apple マップと同じ形)。
全面にすると、**「押した点」と「出てきた面」のつながりが切れます**。
**見出しバーをつまんで、高さを変えられます**。上へ引けば全面、下へ引けば元の高さ、
さらに下へ引くと閉じます。**指に追いて動きます**——しきい値だけで切り替えると、
離すまで何も起きず、**やはり動かないものに見えます**
(実際に「下から出る風で、実際動かない」という指摘を受けています・2026-09-29)。
**中身は作り直さないので、めくった位置はそのまま**です。
**狭い画面では最初から広げて開きます**(幅 360px の吹き出しに写真は入らないため)。
> **この部品は、どこへも何も送りません**。「利用者がどこを見たかが第三者に渡らない」が
> MMJ の主張なので、部品が勝手に送り始めたら、その主張が嘘になります。
> 何を数えるかは、`mmj-poi-click` を受けた側が決めてください。
#### 出来事(数えたいとき)
| 出来事 | いつ | `detail` |
| --- | --- | --- |
| `mmj-poi-click` | **点が押された**とき | `properties` / `lngLat` |
| `mmj-card-open` | **カードが開いた**とき(押した・送った・URL から復元した、すべて) | `properties` / `lngLat` / `index` / `total` / `id` |
| `mmj-card-step` | **[‹] [›] で送った**とき | `delta`(次へ 1・前へ -1) / `index` / `total` |
**数えるなら `mmj-card-open` です**。`mmj-poi-click` は「点が押された」ことしか
言いません。送って回ったぶんも、共有 URL から開いたぶんも入りません。
```js
document.addEventListener('mmj-card-open', (event) => {
// 例: 自分の集計へ。**MMJ はどこへも送りません**
console.log(event.detail.id, event.detail.index, '/', event.detail.total);
});
```
> **`document` でも、`` でも、`` そのものでも受け取れます。**
> どれで待っても **1 回だけ**届きます。
- **押すと `mmj-poi-click` が飛びます**(`detail.properties` / `detail.lngLat`)。
何を出すかは使う側が決めます。
- **名前は衝突したら消えます**(`text-allow-overlap: false`)。重ねて出しても読めないためで、
**点は残るので場所は分かります**。実物(`2026-09-17-poi-z15.jpg`)でも 8 点中 1 つの名前が
衝突で消えています。
- `` との使い分け: **まとめたいときはクラスタ、1 点ずつ名前を出したいときはこちら。**
## この部品が引き受けていること
1. `pmtiles` プロトコルの登録(1 回だけ)
2. スタイルの取得と `__TILES_URL__` の差し替え(**組み立てるのは URL だけで、色ではない**)
3. **帰属表示**(`attributionControl` を消せない形で渡す・ODbL)と
**CJK フォントの既定**(`localIdeographFontFamily`)
4. 失敗したときに**白い地図ではなく理由を画面へ出す**
5. 子要素への map の受け渡し(`mmj-ready` を待つので、読み込み順で崩れない)
## 厚さの判断(PRD §3 の止め条件)
**素の MapLibre を呼ぶのと変わらない厚さなら、この部品は要りません。**
実測で比べます。
| 画面 | 地図を出すために書いた JS |
|---|---|
| `apps/demo/index.html`(素の MapLibre) | 約 40 行 |
| `apps/demo/elements.html`(部品) | **10 行**(属性を配信元へ向ける部分だけ。目印 3 つは HTML) |
差が出ているのは、上の 1〜4 が**呼ぶ側から見えなくなる**ためです。
特に **2 と 3 は、忘れても画面が白くならずに静かに間違う**(帰属表示が消える・
字が出ない)ので、既定に埋める価値があります。
**ここから厚くしない**のが約束です。地図の状態を抱え込む、独自のイベント体系を作る、
スタイルを組み立てる — これらをやり始めたら、素の MapLibre より薄いという根拠が消えます。
## 実物


200 点を `` で置いたところ(z11 でまとまり、z15 でばらける)。
**この 200 点は合成データで、実在の場所ではありません**(`apps/demo/data/README.md`)。


自前の POI を `shop_name` から出したところ。**中央の丸い建物(京セラドーム)に名前が
付いていないのは、ベースのスタイルに POI レイヤが無いから**です。
**配るスタイルは駅だけを描いています**(駅以外の POI は、置く側が自分で足します)。

## まだやっていないこと(S2 の残り)
- **React / Vue のラッパ**(S3 / S4)。Web Components はそのまま使えるので、
ラッパが薄くならないなら作りません。
## 実装のときに実測で見つけたこと
- **暗いページに置くと、ポップアップの文字が読めなくなる。**
MapLibre のポップアップは白地で、文字色をページから継承します。
暗いページ(`color: #e7e9ee` など)だと白地に明るい文字になります。
部品側で文字色だけ戻しています(`.mmj-popup`)。**配色を決めているのではなく、
MapLibre が元々想定している対比へ戻しているだけ**です。
- **属性は部品を読み込む前に入れる。** 逆にすると、属性が付く前に
`connectedCallback` が走り「配信先が無い」と言って終わります。
- **`mmj-ready` の時点では、まだ source を足せない。** `mmj-ready` は Map を作った直後に
出ますが、そこではスタイルがまだ読み終わっていません。`addSource` すると例外になります。
`` は `isStyleLoaded()` を見て、必要なら `load` を待ってから足しています。
- **GeoJSON を配るとき、content-type が `application/octet-stream` だと紛らわしい。**
MapLibre は中身を見に行くので描けますが、掴んだ側が何のファイルか分かりません。
`tools/serve` に `.geojson` → `application/geo+json` を足しました。
### `hash` と `pitch` を一緒に使うとき
**MapLibre の hash は `#zoom/lat/lng/bearing/pitch` の 5 要素です。**
3 要素しか書かれていない URL を開くと、**bearing と pitch が 0 に戻されます。**
そのままだと、**3D のページを URL で渡した相手の画面では建物が平らになります。**
実際にそうなりました(2026-09-20・撮って気づいた)。
```
#16/34.7024/135.4959 → pitch 0 に戻る。建物が立たない
#16/34.7024/135.4959/0/60 → 立つ
```
**部品側で当て直しています**。hash が pitch を持っていないときだけ、
属性の `pitch` を地図へ当て直します。**持っているときは触りません**
(利用者が URL で指定した角度を奪わないため)。
## `` — 地図に重ねる画像タイル
```html
```
**1 つで広く効きます**。雨雲レーダー・ハザードマップ・地盤・震度・空中写真は、
どれもラスタタイルで配られています。
| 属性 | 意味 |
| --- | --- |
| `src` | タイルの URL(必須)。**`{z}` `{x}` `{y}` が揃っていること** |
| `attribution` | **そのデータの出典と時点**。地図の中に出ます |
| `opacity` | 濃さ 0〜1。**既定 0.7** |
| `min-zoom` / `max-zoom` | **持っているタイルの倍率**。source に書きます(後述) |
| `frames` | 時刻の見出しを読点区切りで(`"09:00,10:00,…"`)。**時間で動かす** |
| `interval` | 送りの間隔(ミリ秒)。既定 700 |
| `autoplay` | 置いただけで動き出す(真偽の属性) |
| `before` | このレイヤの下へ入れる。**既定はいちばん手前のラベルの下** |
| `tile-size` | 既定 256 |
| `layer-id` | source 名。**1 枚に 2 つ重ねるときは分ける** |
### 既定が「透ける」「ラベルの下」である理由
**不透明のまま重ねると、その色がどこの話なのか読めません**。浸水想定でも雨雲でも、
**現在地と道が透けて見えて初めて使えます**。
**ラベルの上に重ねると、地名も駅名も消えます**。既定でいちばん手前のラベルの下へ入れます。
### 読めない URL は落とします
`{z}` `{x}` `{y}` のどれかが欠けていると、MapLibre は**同じ 1 枚を延々と貼り続けます**。
地図は出るので**見ても間違いだと分かりません**。`console.error` を出して、重ねません
(**地図は殺しません**)。
### MMJ はデータを持ちません
出どころを指すのも、出典を名乗るのも導入者です。**MMJ は判断しません** ——
重ねた色が何を意味するかも、いつ時点のものかも解釈しません。
> **気象のデータを重ねるときの注意**。気象庁の予報をそのまま出すだけなら
> 予報業務許可は要りませんが、**独自の予想を足すと許可が要ります**(気象業務法)。
> 「この降り方なら 30 分後にここが危ない」のような表示は、そこに入ります。
> **国土地理院のタイルを重ねるときの注意**。測量成果の利用にあたるかどうかは
> 導入者側の判断になります(MMJ 本体はこれを同梱しません。`PRD.md` §2)。
## 防災・避難で使うときの約束
**この分野では、精度と出典を厳しく扱ってください**(2026-09-27・人の判断)。
地図は単体で持ち歩かれ、**間違っていると人が危ない**種類のデータだからです。
### 1. 出典と**時点**を、地図の中に出す
### 時間で動かす(雨雲レーダー)
```html
```
`src` の `{t}` が**時刻の番号**(0 始まり)に置き換わります。見出し(`09:00` など)は
置く側が書きます——「10 分後」なのか「10/7 10 時」なのかは、置く側しか知りません。
**時刻ぶんをまとめて置き、見せ方だけを切り替えます**。時刻を変えるたびに source を
作り直すと、切り替えのたびに取りに行って白く抜けます。置いておけば、2 周目からは
控えが効いて紙芝居のように進みます。
頁側から操作するには:
| | |
| --- | --- |
| `showFrame(n)` | n 番目へ送る |
| `play()` / `pause()` | 送りを始める・止める |
| `frames` | 見出しの一覧 |
| `frame` | いま何番目か |
| `mmj-raster-frame` | 送られたときに飛ぶ(`detail.index` / `detail.label`) |
実物:
### `min-zoom` / `max-zoom` は「持っている倍率」
**layer ではなく source に書きます**。layer に書くと「その範囲の外では出さない」に
なるだけで、**無い倍率のタイルを取りに行き続けます**(404 が並びます)。
source に書くと、持っている倍率を拡大・縮小して見せてくれます。
2026-10-07 に実際に踏みました。z8 だけ置いて layer に `max-zoom` を付けた頁で、
z6/z7 への要求が **188 件 404** になりました。さらに `min-zoom` を下回る倍率で開くと、
**1 滴も描かれません**(タイルはあるのに真っ新に見えます)。
## `` — 台風を、報道で見る形で描く
```html
```
| 属性 | 意味 |
| --- | --- |
| `src` | 進路のデータの URL(必須) |
| `gale-opacity` / `storm-opacity` | 面の濃さ。既定 0.22 / 0.38 |
| `before` | このレイヤの下へ入れる |
| `layer-id` | source 名。1 枚に 2 つ置くときは分けます |
データの形:
```json
{
"frames": [
{ "label": "09:00", "center": [131.0, 30.0], "pressure": 985, "stormKm": 60, "galeKm": 220 },
{ "label": "15:00", "center": [134.8, 33.3], "pressure": 950, "stormKm": 129, "galeKm": 379,
"forecast": true, "forecastKm": 68 }
]
}
```
| | |
| --- | --- |
| `center` | `[経度, 緯度]`(GeoJSON と同じ並び・必須) |
| `stormKm` / `galeKm` | 暴風域・強風域の半径(km) |
| `forecastKm` | 予報円の半径(km) |
| `forecast` | `true` にすると、そこから先の進路線が破線になります |
**中心を読めない時刻は落とします**。途中が欠けていても、残りは描けます。
### 描く順と色
強風域(黄)→ 暴風域(赤)→ 予報円(白の破線)→ 進路線 → 中心、の順で重ねます。
**順番には意味があります**——強風域をあとに置くと、暴風域が隠れます。
**色は作っていません**。黄と赤は外界(気象庁の配色)が決めているものを写しています。
独自の配色にすると、赤が弱い側に見えるような誤読を生みます。
### 予報円は「台風の大きさ」ではありません
予報円は**中心の位置の不確かさ**で、先へ行くほど大きくなります。暴風域とは別物です。
**塗らずに破線で描いている**のはそのためで、塗ると「この範囲が暴風」と読まれます。
### 時刻を送るのは置く側
```js
const typhoon = document.querySelector('mmj-typhoon');
typhoon.showFrame(3); // 3 番目へ
typhoon.addEventListener('mmj-typhoon-frame', (e) => console.log(e.detail.label, e.detail.pressure));
```
`` と同じ再生に合わせられるように、送りは持っていません。
実物(`rain.html`)では、雨雲の `mmj-raster-frame` を受けて同じ番号を渡しています。
**MMJ は予報しません**。渡された点を描くだけで、外挿も補間もしません。
実物:
`` にも `` にも `attribution` があります。**配布元のページに
書いても届きません**——オフラインで端末に入ったあと、そのページは付いて来ません。
### 2. **制度に依存する語**を出すなら、いつの制度かも書く
警戒レベル・警報名・避難情報の区分は**変わります**。
「レベル 4」とだけ書いてあると、読んだ人はいつの制度の話か確かめようがありません。
```html
```
### 3. 架空のものは、**架空だと明示する**
訓練用に作った地点や経路を、実在のものに見える形で置かないでください。
**実在のデータに見えるものを作り置くと、いつか誰かがそれを本物として使います。**
MMJ のデモもそうしています(`apps/demo/data/README.md`)。
### 4. **確かめていないものは、確かめていないと書く**
型だけ合わせて中身を埋めると、**読んだ人には確かめ済みと区別がつきません。**
### 5. **この手引き自身も、情報源ではありません**
上の例に出てくる日付や自治体名は、**書き方を示すための穴埋め**です(`○年○月`)。
**確かめていない具体的な数値を、例示として置きません**——例示は写されるからです。
**MMJ は地図を描く道具であって、情報源ではありません。**
同じ理由で、警戒レベルの一覧表のような**制度の中身をここに持ちません**。
持てば維持する義務が生まれ、**古くなった表が「MMJ が言っている」形で残ります**。
> **MMJ 側は判断しません**。渡されたものを描くだけで、正しさはデータの出どころが持ちます。
> **だからこそ、出どころを名乗れる場所を部品が用意しています。**
## `` — 面を値で塗り分ける
浸水深・地盤・震度・土砂災害の区域のような、**値を持つ面**を重ねます。
```html
```
| 属性 | 意味 |
| --- | --- |
| `src` | 面の GeoJSON の URL(必須) |
| `value-key` | **どの属性で塗り分けるか**(必須)。文字で入っていても数として読みます |
| `steps` | `"しきい値:色,…"`(必須)。**小さい順に並べ直します** |
| `legend` | 凡例の見出し。**渡さなければ凡例は出ません**(下記) |
| `opacity` | 透かし具合。既定 0.55。**下の地図が読めなくなると避難に使えません** |
| `before` | この layer の下に入れる。既定は**最初のラベル層の下** |
| `attribution` | **出典と時点**。`© OpenStreetMap contributors` の隣に出ます |
### 色はこちらで作りません
**ハザードマップの配色は、国や自治体が決めた意味のある色**です
(浸水深の青、土砂災害の黄と赤)。**独自配色に置き換えると誤読を生みます**
——`CLAUDE.md`「外界が色を決めているものは、配色ルールの適用外」。
`steps` は**置く側が渡すもの**で、MMJ は並べ替えて塗るだけです。
### しきい値より下は塗りません
`0.1m の浸水` を薄い青で塗ると、**浸水していない場所と見分けがつかない**まま
色だけが付きます。最初のしきい値に届かない地物と、**値を持たない地物**は
透明のままにしてあります。
### 凡例は、見出しを渡したときだけ出ます
**色だけ出しても、濃い青が何メートルかは誰にも分かりません。**
ただし**単位も見出しも知らないまま数字だけ並べると、読んだ人が意味を作ってしまう**ので、
`legend` を渡したときだけ出します。区間で読ませ、**最後は「以上**」にしています
(上限を書くと、そこで終わると読めてしまうため)。
> ### MMJ は判断しません
>
> **「ここは危ない」とも「逃げろ」とも言いません**。渡された値を、
> 渡された色で塗るところまでです。上の「防災・避難で使うときの約束」を必ず読んでください。
>
> 実物は (**架空の見本**です。
> 四角が規則的に並んでいるのがその印で、実際の浸水想定は地形なりの形になります)。
## `` — ここから半径◯ m
```html
```
**外部サービスもデータも要りません**。幾何だけで出ます。
| 属性 | 意味 |
| --- | --- |
| `center` | `経度,緯度`(必須) |
| `radius` | 半径(**m**・必須)。0 以下は描きません |
| `color` | 面と縁の色。既定は ``、無ければ淡い青灰 |
| `fill-opacity` | 面の濃さ 0〜1。**既定 0.12** |
| `steps` | 円周の分割数。既定 96 |
| `before` | このレイヤの下へ入れる |
| `layer-id` | source 名。**何重にも置くときは分ける** |
### 画面上の円を使っていません
MapLibre の `circle-radius` は**画面のピクセル**なので、倍率を変えても大きさが変わりません。
**「半径 800 m」と書いてあるのに 800 m を指していない図**になります。
ここでは**地表の点を並べた多角形**を作ります。**倍率を変えても同じ場所を囲みます。**
**高緯度では画面上が縦長に見えます**。それは歪みではなく、メルカトル図法の側の都合です
(地表の距離は変わりません。テストで独立に測り直して確かめてあります)。
### 「何分で行けるか」は出しません
それは**経路探索**で、`PRD.md` §2 で作らないと決めています。そして
**災害時はとくに危ない**——災害前の道路データで計算した圏域は、
**崩れた道・浸水した道を「行ける」と言います**。
ここが出すのは**直線距離**だけです。「直線で 800 m」と「歩いて 800 m」は違う、
と読む人に分かる形で使ってください。