増井俊之
https://lh3.googleusercontent.com/a/ACg8ocK3P3R57l7lVDIgWPb5wu_XfBHWQkg22jsft1hMitt1fZ55XeMh=s96-c#.png
code:script.jss
// Scrapbox MapFeel - 位置情報付きページを地図上に表示する UserScript
// settings ページに code:script.js として貼り付けてください
const COORD_RE = /\[N(\d.+),E(\d.+),Z\d+\]/g;
let mapContainer = null;
let leafletLoaded = false;
function mapfeel_getProject() {
return scrapbox.Project.name;
}
async function mapfeel_fetchAllPages(project) {
let skip = 0;
const limit = 100;
const allPages = [];
while (true) {
const res = await fetch(
/api/pages/${encodeURIComponent(project)}?skip=${skip}&limit=${limit}
);
if (!res.ok) throw new Error(API error: ${res.status});
const data = await res.json();
allPages.push(...data.pages);
if (data.pages.length < limit) break;
skip += limit;
}
return allPages;
}
function mapfeel_parseCoords(text) {
const coords = [];
let m;
const re = new RegExp(COORD_RE.source, "g");
while ((m = re.exec(text)) !== null) {
coords.push({ lat: parseFloat(m1), lng: parseFloat(m2) });
}
return coords;
}
function mapfeel_loadLeaflet() {
if (leafletLoaded) return Promise.resolve();
return new Promise((resolve) => {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css";
document.head.appendChild(link);
const script = document.createElement("script");
script.src = "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js";
script.onload = () => {
leafletLoaded = true;
resolve();
};
document.head.appendChild(script);
});
}
function mapfeel_escapeHtml(str) {
const d = document.createElement("div");
d.textContent = str;
return d.innerHTML;
}
function mapfeel_createUI() {
if (mapContainer) {
mapContainer.style.display =
mapContainer.style.display === "none" ? "flex" : "none";
return;
}
mapContainer = document.createElement("div");
mapContainer.id = "mapfeel-overlay";
mapContainer.innerHTML = `
<div id="mapfeel-panel">
<div id="mapfeel-header">
<span id="mapfeel-title">MapFeel - 位置情報マップ</span>
<span id="mapfeel-status"></span>
<button id="mapfeel-close">&times;</button>
</div>
<div id="mapfeel-map"></div>
</div>
`;
const style = document.createElement("style");
style.textContent = `
#mapfeel-overlay {
position: fixed;
inset: 0;
z-index: 99999;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
}
#mapfeel-panel {
width: 90vw;
height: 85vh;
background: #fff;
border-radius: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 4px 24px rgba(0,0,0,0.3);
}
#mapfeel-header {
display: flex;
align-items: center;
padding: 10px 16px;
background: #4a90d9;
color: #fff;
font-size: 14px;
gap: 12px;
}
#mapfeel-title { font-weight: bold; }
#mapfeel-status {
flex: 1;
text-align: right;
font-size: 12px;
opacity: 0.9;
}
#mapfeel-close {
background: none;
border: none;
color: #fff;
font-size: 22px;
cursor: pointer;
padding: 0 4px;
line-height: 1;
}
#mapfeel-close:hover { opacity: 0.7; }
#mapfeel-map { flex: 1; }
.leaflet-tooltip { font-size: 16px; }
`;
document.head.appendChild(style);
document.body.appendChild(mapContainer);
document.getElementById("mapfeel-close").addEventListener("click", () => {
mapContainer.style.display = "none";
});
mapContainer.addEventListener("click", (e) => {
if (e.target === mapContainer) mapContainer.style.display = "none";
});
mapfeel_loadAndShow();
}
async function mapfeel_loadAndShow() {
const project = mapfeel_getProject();
const statusEl = document.getElementById("mapfeel-status");
await mapfeel_loadLeaflet();
const map = L.map("mapfeel-map").setView(36.5, 138.0, 6);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "&copy; OpenStreetMap contributors",
maxZoom: 19,
}).addTo(map);
statusEl.textContent = "ページを取得中...";
const pages = await mapfeel_fetchAllPages(project);
const markers = [];
for (const page of pages) {
const descText = (page.descriptions || []).join("\n");
const coords = mapfeel_parseCoords(descText);
for (const c of coords) {
const marker = L.marker(c.lat, c.lng).addTo(map);
const url = https://scrapbox.io/${encodeURIComponent(project)}/${encodeURIComponent(page.title)};
const descLines = (page.descriptions || [])
.filter((line) => !COORD_RE.test(line) && !/https?:\/\/\S+/.test(line))
.map(mapfeel_escapeHtml)
.join("<br>");
const imageHtml = page.image ? <img src="${page.image}" style="max-width:200px;max-height:150px;display:block;margin-top:4px;"> : "";
const tooltipHtml = <b>${mapfeel_escapeHtml(page.title)}</b>${descLines ? "<br>" + descLines : ""}${imageHtml};
marker.bindTooltip(tooltipHtml);
marker.on("click", () => { location.href = url; });
markers.push(marker);
}
}
statusEl.textContent = ${markers.length}件の位置情報を表示中;
if (markers.length > 0) {
const group = L.featureGroup(markers);
map.fitBounds(group.getBounds().pad(0.1));
}
}
// 右下にボタンを追加
{
const btn = document.createElement("button");
btn.id = "mapfeel-btn";
btn.textContent = "\u{1F5FA}";
btn.title = "MapFeel - 位置情報マップを表示";
Object.assign(btn.style, {
position: "fixed",
bottom: "20px",
right: "20px",
zIndex: "9999",
width: "48px",
height: "48px",
borderRadius: "50%",
border: "none",
background: "#4a90d9",
color: "#fff",
fontSize: "22px",
cursor: "pointer",
boxShadow: "0 2px 8px rgba(0,0,0,0.3)",
display: "flex",
alignItems: "center",
justifyContent: "center",
});
btn.addEventListener("click", mapfeel_createUI);
document.body.appendChild(btn);
}