diff options
| -rw-r--r-- | scripts/park-editor.mjs | 98 |
1 files changed, 94 insertions, 4 deletions
diff --git a/scripts/park-editor.mjs b/scripts/park-editor.mjs index 48b9f17..2ccd96d 100644 --- a/scripts/park-editor.mjs +++ b/scripts/park-editor.mjs @@ -83,6 +83,8 @@ function findCorruptions(value, path, out) { return out; } +const FFFD = "\uFFFD"; + function recordKey(obj) { const src = obj && obj.properties ? obj.properties : obj; if (!src) return null; @@ -93,9 +95,92 @@ function recordKey(obj) { return null; } +function canonicalKeysFrom(lists) { + const keys = new Set(); + for (const list of lists) { + for (const rec of list || []) { + const src = rec && rec.properties ? rec.properties : rec; + if (!src) continue; + for (const key of Object.keys(src)) { + if (!key.includes(FFFD)) keys.add(key); + } + } + } + return [...keys]; +} + +// 「ベンチ・椅���」のような項目名の文字化けを、前後の文字から正しい項目名に戻す +function normalizeRecordKeys(rec, canonicalKeys) { + const src = rec && rec.properties ? rec.properties : rec; + if (!src) return; + for (const bad of Object.keys(src)) { + if (!bad.includes(FFFD)) continue; + const prefix = bad.split(FFFD)[0]; + const suffix = bad.split(FFFD).slice(-1)[0]; + const candidates = canonicalKeys.filter( + (key) => key.startsWith(prefix) && key.endsWith(suffix), + ); + if (candidates.length !== 1) continue; + const good = candidates[0]; + const value = src[bad]; + if (!(good in src)) { + src[good] = value; + delete src[bad]; + } else if (!src[good] && value) { + src[good] = value; + delete src[bad]; + } else if (src[good] === value) { + delete src[bad]; + } + } +} + +// 値の文字化けが「何文字かの欠落」だけなら、ディスク側の値で直す +function repairValue(posted, current) { + if (typeof posted !== "string" || !posted.includes(FFFD)) return posted; + if (typeof current !== "string" || !current) return posted; + const parts = posted.split(/\uFFFD+/); + if (parts.length < 2) return posted; + const pattern = + "^" + + parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("(.{1,4}?)") + + "$"; + return new RegExp(pattern, "s").test(current) ? current : posted; +} + +function mergeInto(target, source) { + const dst = target.properties || target; + const src = source.properties || source; + for (const [field, value] of Object.entries(src)) { + if (!(field in dst) || (!dst[field] && value)) dst[field] = value; + } +} + // ブラウザが古いデータを持っていても、後から足した項目やレコードを消さないようにする。 // 画面になくて編集できないキーは、ディスク側の値を残す。 -function mergeMissing(postedList, currentList) { +function mergeMissing(postedList, currentList, canonicalKeys) { + for (const rec of postedList) normalizeRecordKeys(rec, canonicalKeys); + + // 同じレコードが重複していたら1つにまとめる + const deduped = []; + const byKey = new Map(); + for (const rec of postedList) { + const key = recordKey(rec); + if (!key) { + deduped.push(rec); + continue; + } + const existing = byKey.get(key); + if (existing) { + mergeInto(existing, rec); + continue; + } + byKey.set(key, rec); + deduped.push(rec); + } + postedList.length = 0; + for (const rec of deduped) postedList.push(rec); + const map = new Map(); for (const rec of currentList) { const key = recordKey(rec); @@ -111,7 +196,11 @@ function mergeMissing(postedList, currentList) { 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; + if (!(field in dst)) { + dst[field] = value; + } else if (typeof dst[field] === "string" && dst[field].includes(FFFD)) { + dst[field] = repairValue(dst[field], value); + } } } for (const [key, rec] of map) { @@ -170,10 +259,11 @@ const server = createServer(async (req, res) => { // 古い画面からの保存で、新しい項目・レコードが消えないようにマージする const current = JSON.parse(await readFile(target.dataset.path, "utf-8")); + const canonicalKeys = canonicalKeysFrom([current, data.features || data]); if (target.id === "nearbyParks") { - mergeMissing(data.features, current.features || []); + mergeMissing(data.features, current.features || [], canonicalKeys); } else { - mergeMissing(data, current); + mergeMissing(data, current, canonicalKeys); } const text = target.dataset.pretty |
