From 1495a79dd29e0d4c370086aec9dfb146c7f24bc3 Mon Sep 17 00:00:00 2001
From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>
Date: Thu, 29 Oct 2026 14:44:18 +0900
Subject: park-editor: 列の追加と文字化け項目の表示
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
スイング遊具にロッキンパッピー等を含め、シェルター列を2行に、緯度・経度を最小幅にした。草地広場・多目的広場の列を追加。文字化けした項目名をバナー表示し、古い画面からの保存で新項目・新レコードが消えないようマージする。
---
scripts/park-editor.html | 42 +++++++++++++++++++++++++++++++++++++-----
scripts/park-editor.mjs | 44 ++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 81 insertions(+), 5 deletions(-)
(limited to 'scripts')
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; }
@@ -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("シェルター等", "シェルター・パーゴラ・
東屋"),
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 = "" + ["#", "確認", "地図"].concat(fields.map((f) => f.label)).map((label) => "| " + label + " | ").join("") + "
";
+ 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 =
+ "" +
+ headCols.map((col) => `| ${col.label} | `).join("") +
+ "
";
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`;
--
cgit v1.3.1