diff options
| author | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-10-29 14:44:18 +0900 |
|---|---|---|
| committer | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-10-29 14:44:18 +0900 |
| commit | 1495a79dd29e0d4c370086aec9dfb146c7f24bc3 (patch) | |
| tree | 0f6231a105be157dda754b9e2f57b2019298dc9e | |
| parent | ed970d8dd6dbbaded7f1f45963ef54049f3571b9 (diff) | |
park-editor: 列の追加と文字化け項目の表示
スイング遊具にロッキンパッピー等を含め、シェルター列を2行に、緯度・経度を最小幅にした。草地広場・多目的広場の列を追加。文字化けした項目名をバナー表示し、古い画面からの保存で新項目・新レコードが消えないようマージする。
| -rw-r--r-- | scripts/park-editor.html | 42 | ||||
| -rw-r--r-- | scripts/park-editor.mjs | 44 |
2 files changed, 81 insertions, 5 deletions
diff --git a/scripts/park-editor.html b/scripts/park-editor.html index e320e5e..2bcb7ea 100644 --- a/scripts/park-editor.html +++ b/scripts/park-editor.html @@ -47,6 +47,8 @@ #clear-checks { padding: .3rem .6rem; } td.mapcell { text-align: center; } td.mapcell a { text-decoration: none; padding: 0 .4rem; cursor: pointer; } + th.c-coord, td.c-coord { width: 6ch; max-width: 6ch; overflow: hidden; } + td.c-coord input { width: 6ch; min-width: 0; font-size: .72rem; } </style> </head> <body> @@ -79,7 +81,7 @@ const TOY_ORDER = [ ["slide", "滑台"], ["sand", "砂場"], ["bar", "鉄棒"], - ["spring", "スプリング遊具"], + ["spring", "スイング遊具"], ["complex", "複合遊具"], ["other", "その他遊具"], ]; @@ -89,7 +91,7 @@ const TOY_TOKENS = { slide: ["滑台", "滑り台", "すべり台"], sand: ["砂場"], bar: ["鉄棒"], - spring: ["スプリング"], + spring: ["スプリング", "スイング", "ロッキンパッピー"], complex: ["複合遊具"], }; @@ -101,9 +103,10 @@ const DATASETS = { FIELD("面積㎡", "面積㎡"), FIELD("種別", "種別"), FIELD("設置者", "設置者"), FIELD("状態", "状態"), FIELD("トイレ", "トイレ"), FIELD("水飲み場", "水飲み場"), FIELD("ベンチ・椅子", "ベンチ・椅子"), - FIELD("テーブル", "テーブル"), FIELD("シェルター等", "シェルター・パーゴラ・東屋"), + FIELD("テーブル", "テーブル"), FIELD("シェルター等", "シェルター・パーゴラ・<br>東屋"), FIELD("防災用具", "防災用具"), FIELD("ボール遊び", "ボール遊び"), ...TOY_ORDER.map(([toy, label]) => ({ key: `__toy_${toy}`, label, toy })), + FIELD("草地広場", "草地広場"), FIELD("多目的広場", "多目的広場"), FIELD("時計", "時計"), FIELD("緯度", "緯度"), FIELD("経度", "経度"), FIELD("備考", "備考"), ], @@ -394,6 +397,7 @@ async function load(id) { isFC = data.type === "FeatureCollection"; rows = isFC ? data.features : data; render(); + showCorruptKeys(); statusEl.textContent = `${DATASETS[id].label}: ${rows.length}件`; } catch (e) { statusEl.textContent = "読み込みに失敗: " + e.message; @@ -401,7 +405,16 @@ async function load(id) { } function render() { - thead.innerHTML = "<tr>" + ["#", "確認", "地図"].concat(fields.map((f) => f.label)).map((label) => "<th>" + label + "</th>").join("") + "</tr>"; + const COORD_KEYS = new Set(["緯度", "経度"]); + const headCols = ["#", "確認", "地図"] + .map((label) => ({ label, cls: "" })) + .concat( + fields.map((f) => ({ label: f.label, cls: COORD_KEYS.has(f.key) ? "c-coord" : "" })), + ); + thead.innerHTML = + "<tr>" + + headCols.map((col) => `<th class="${col.cls}">${col.label}</th>`).join("") + + "</tr>"; tbody.innerHTML = ""; rows.forEach((row, r) => { const props = isFC ? row.properties || {} : row; @@ -444,6 +457,7 @@ function render() { fields.forEach((field, c) => { const td = document.createElement("td"); + if (field.key === "緯度" || field.key === "経度") td.className = "c-coord"; const inp = document.createElement("input"); inp.value = field.toy ? toys[field.toy] : (props[field.key] != null ? props[field.key] : ""); inp.dataset.r = r; @@ -481,6 +495,24 @@ function filter() { count.textContent = shown + " / " + rows.length + " 件"; } +// 項目名(キー)の文字化けは表に出ないため、バナーで知らせる +function showCorruptKeys() { + const hits = []; + rows.forEach((row, i) => { + const props = isFC ? row.properties || {} : row; + for (const key of Object.keys(props)) { + if (!key.includes("\uFFFD")) continue; + const name = props["公園名"] || props["name"] || props["名称"] || `行${i + 1}`; + hits.push(`${name} / ${key}`); + } + }); + if (!hits.length) return; + warningEl.hidden = false; + warningEl.textContent = `文字化けした項目名が ${hits.length} 件あります: ${hits.slice(0, 5).join(" / ")}${hits.length > 5 ? " …" : ""}`; + warningEl.style.background = "#ffcdd2"; + warningEl.style.color = "#8e0000"; +} + function markCorrupt() { tbody.querySelectorAll("input[data-r][data-c]").forEach((inp) => { inp.classList.toggle("corrupt", inp.value.includes("\uFFFD")); @@ -498,7 +530,7 @@ function corruptLabel(inp) { const props = isFC ? row.properties || {} : row; const name = props["公園名"] || props["name"] || props["名称"] || `行${Number(inp.dataset.r) + 1}`; - const label = fields[Number(inp.dataset.c)].label; + const label = fields[Number(inp.dataset.c)].label.replace(/<[^>]+>/g, ""); return `${name} / ${label}: ${inp.value}`; } diff --git a/scripts/park-editor.mjs b/scripts/park-editor.mjs index e3bae45..48b9f17 100644 --- a/scripts/park-editor.mjs +++ b/scripts/park-editor.mjs @@ -83,6 +83,42 @@ function findCorruptions(value, path, out) { return out; } +function recordKey(obj) { + const src = obj && obj.properties ? obj.properties : obj; + if (!src) return null; + for (const key of ["公園名", "name", "名称", "our_name"]) { + const value = src[key]; + if (typeof value === "string" && value) return value; + } + return null; +} + +// ブラウザが古いデータを持っていても、後から足した項目やレコードを消さないようにする。 +// 画面になくて編集できないキーは、ディスク側の値を残す。 +function mergeMissing(postedList, currentList) { + const map = new Map(); + for (const rec of currentList) { + const key = recordKey(rec); + if (key) map.set(key, rec); + } + const postedKeys = new Set(); + for (const rec of postedList) { + const key = recordKey(rec); + if (!key) continue; + postedKeys.add(key); + const current = map.get(key); + if (!current) continue; + const dst = rec.properties || rec; + const src = current.properties || current; + for (const [field, value] of Object.entries(src)) { + if (!(field in dst)) dst[field] = value; + } + } + for (const [key, rec] of map) { + if (!postedKeys.has(key)) postedList.push(rec); + } +} + const server = createServer(async (req, res) => { try { const url = (req.url || "").split("?")[0]; @@ -132,6 +168,14 @@ const server = createServer(async (req, res) => { throw new Error("データが配列ではありません"); } + // 古い画面からの保存で、新しい項目・レコードが消えないようにマージする + const current = JSON.parse(await readFile(target.dataset.path, "utf-8")); + if (target.id === "nearbyParks") { + mergeMissing(data.features, current.features || []); + } else { + mergeMissing(data, current); + } + const text = target.dataset.pretty ? `${JSON.stringify(data, null, 2)}\n` : `${JSON.stringify(data)}\n`; |
