map-editor / index.html
soiz1's picture
Update index.html
e730b57 verified
<!DOCTYPE html>
<html lang="ja">
<head>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@400;700&display=swap" rel="stylesheet">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>HACKER MAP EDITOR</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css">
<script src="https://cdn.tailwindcss.com"></script>
<style>
:root {
--hacker-primary: #00ffff;
--hacker-secondary: #0088ff;
--hacker-bg: #001a33;
--hacker-text: #e0e0e0;
--hacker-accent: #ff00ff;
--hacker-border: #0066ff;
}
body {
font-family: 'Source Code Pro', monospace;
background-color: var(--hacker-bg);
color: var(--hacker-text);
background-image: radial-gradient(circle at 10% 20%, rgba(0, 180, 255, 0.05) 0%, rgba(0, 50, 100, 0.1) 90%);
min-height: 100vh;
overflow-x: hidden;
}
#save-map-modal {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 20, 40, 0.95);
border: 1px solid var(--hacker-border);
padding: 2rem;
z-index: 2001;
width: 80%;
max-width: 500px;
}
#save-map-modal h3 {
color: var(--hacker-primary);
margin-bottom: 1rem;
text-align: center;
}
#save-map-name {
width: 100%;
margin-bottom: 1rem;
background: rgba(0, 10, 20, 0.8);
border: 1px solid var(--hacker-border);
color: var(--hacker-text);
padding: 0.5rem;
}
.hacker-header {
background: linear-gradient(90deg, rgba(0, 40, 80, 0.8) 0%, rgba(0, 80, 160, 0.6) 100%);
border-bottom: 1px solid var(--hacker-border);
box-shadow: 0 0 15px rgba(0, 200, 255, 0.3);
padding: 1rem;
margin-bottom: 1rem;
position: relative;
overflow: hidden;
}
.hacker-header::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(90deg,
transparent 0%,
rgba(0, 255, 255, 0.1) 50%,
transparent 100%);
animation: scanline 5s linear infinite;
}
@keyframes scanline {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
#map {
height: 600px;
width: 100%;
border: 2px solid var(--hacker-border);
box-shadow: 0 0 20px rgba(0, 200, 255, 0.4);
filter: hue-rotate(0deg) saturate(1.2);
transition: all 0.3s ease;
}
#map:hover {
box-shadow: 0 0 30px rgba(0, 200, 255, 0.6);
}
#marker-editor {
display: none;
position: absolute;
top: 10px;
left: 10px;
background: rgba(0, 20, 40, 0.95);
padding: 1.5rem;
border-radius: 0;
border: 2px solid var(--hacker-border);
box-shadow: 0 0 20px rgba(0, 200, 255, 0.5);
z-index: 1000;
cursor: move;
font-family: 'Source Code Pro', monospace;
color: var(--hacker-text);
width: 350px;
max-height: 80vh;
overflow-y: auto;
}
#marker-editor h3 {
color: var(--hacker-primary);
text-shadow: 0 0 5px var(--hacker-primary);
border-bottom: 1px solid var(--hacker-border);
padding-bottom: 0.5rem;
margin-bottom: 1rem;
font-weight: 700;
}
#marker-editor label {
color: var(--hacker-secondary);
display: block;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
#marker-editor input,
#marker-editor textarea {
background: rgba(0, 10, 20, 0.9);
border: 1px solid var(--hacker-border);
color: var(--hacker-primary);
padding: 0.5rem;
margin-bottom: 1rem;
width: 100%;
font-family: 'Source Code Pro', monospace;
transition: all 0.3s ease;
}
#marker-editor input:focus,
#marker-editor textarea:focus {
outline: none;
border-color: var(--hacker-primary);
box-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
}
#marker-editor button {
background: linear-gradient(180deg, rgba(0, 100, 200, 0.8) 0%, rgba(0, 50, 150, 0.8) 100%);
color: white;
border: 1px solid var(--hacker-border);
padding: 0.75rem 1.5rem;
margin: 0.5rem 0;
cursor: pointer;
font-family: 'Source Code Pro', monospace;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
transition: all 0.3s ease;
width: 100%;
}
#marker-editor button:hover {
background: linear-gradient(180deg, rgba(0, 150, 255, 0.8) 0%, rgba(0, 80, 180, 0.8) 100%);
box-shadow: 0 0 15px rgba(0, 200, 255, 0.6);
transform: translateY(-2px);
}
#marker-editor button#delete-marker {
background: linear-gradient(180deg, rgba(200, 0, 0, 0.8) 0%, rgba(150, 0, 0, 0.8) 100%);
}
#marker-editor button#delete-marker:hover {
background: linear-gradient(180deg, rgba(255, 50, 50, 0.8) 0%, rgba(200, 0, 0, 0.8) 100%);
}
#icon-preview {
display: none;
margin: 1rem 0;
border: 1px solid var(--hacker-border);
max-width: 100%;
box-shadow: 0 0 10px rgba(0, 200, 255, 0.3);
}
#icon-settings {
display: none;
margin-top: 1rem;
border-top: 1px dashed var(--hacker-border);
padding-top: 1rem;
}
#icon-settings label {
color: var(--hacker-secondary);
margin-bottom: 0.5rem;
}
input[type=range] {
-webkit-appearance: none;
width: 100%;
height: 5px;
background: rgba(0, 50, 100, 0.5);
border-radius: 5px;
margin: 1rem 0;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 15px;
height: 15px;
background: var(--hacker-primary);
border-radius: 50%;
cursor: pointer;
box-shadow: 0 0 5px var(--hacker-primary);
}
input[type=number] {
width: 60px;
margin-left: 1rem;
}
.hacker-btn {
background: linear-gradient(180deg, rgba(0, 100, 200, 0.8) 0%, rgba(0, 50, 150, 0.8) 100%);
color: white;
border: 1px solid var(--hacker-border);
padding: 0.75rem 1.5rem;
margin: 0.5rem;
cursor: pointer;
font-family: 'Source Code Pro', monospace;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
transition: all 0.3s ease;
}
.hacker-btn:hover {
background: linear-gradient(180deg, rgba(0, 150, 255, 0.8) 0%, rgba(0, 80, 180, 0.8) 100%);
box-shadow: 0 0 15px rgba(0, 200, 255, 0.6);
transform: translateY(-2px);
}
.hacker-btn.danger {
background: linear-gradient(180deg, rgba(200, 0, 0, 0.8) 0%, rgba(150, 0, 0, 0.8) 100%);
}
.hacker-btn.danger:hover {
background: linear-gradient(180deg, rgba(255, 50, 50, 0.8) 0%, rgba(200, 0, 0, 0.8) 100%);
}
.hacker-btn.secondary {
background: linear-gradient(180deg, rgba(100, 0, 200, 0.8) 0%, rgba(50, 0, 150, 0.8) 100%);
}
.hacker-btn.secondary:hover {
background: linear-gradient(180deg, rgba(150, 0, 255, 0.8) 0%, rgba(80, 0, 180, 0.8) 100%);
}
.hacker-container {
background: rgba(0, 10, 20, 0.8);
border: 1px solid var(--hacker-border);
padding: 1.5rem;
margin: 1rem 0;
box-shadow: 0 0 15px rgba(0, 100, 200, 0.3);
}
.hacker-title {
color: var(--hacker-primary);
text-shadow: 0 0 5px var(--hacker-primary);
font-weight: 700;
margin-bottom: 1rem;
border-bottom: 1px solid var(--hacker-border);
padding-bottom: 0.5rem;
}
#output-html {
background: rgba(0, 5, 10, 0.9);
border: 1px solid var(--hacker-border);
padding: 1rem;
font-family: 'Source Code Pro', monospace;
color: var(--hacker-primary);
white-space: pre-wrap;
word-break: break-all;
max-height: 300px;
overflow-y: auto;
margin: 1rem 0;
box-shadow: inset 0 0 10px rgba(0, 50, 100, 0.5);
}
#loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 10, 20, 0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 1.5rem;
z-index: 9999;
color: var(--hacker-primary);
text-shadow: 0 0 5px var(--hacker-primary);
}
.loader {
width: 100px;
aspect-ratio: 1;
padding: 10px;
box-sizing: border-box;
display: grid;
filter: blur(5px) contrast(10) hue-rotate(180deg);
mix-blend-mode: lighten;
}
.loader:before,
.loader:after {
content: "";
grid-area: 1/1;
width: 40px;
height: 40px;
background: var(--hacker-primary);
animation: l7 2s infinite;
box-shadow: 0 0 5px var(--hacker-primary);
}
.loader:after {
animation-delay: -1s;
}
@keyframes l7 {
0% { transform: translate(0, 0); }
25% { transform: translate(100%, 0); }
50% { transform: translate(100%, 100%); }
75% { transform: translate(0, 100%); }
100% { transform: translate(0, 0); }
}
.terminal-line {
position: relative;
padding-left: 1.5rem;
margin-bottom: 0.5rem;
}
.terminal-line::before {
content: ">";
position: absolute;
left: 0;
color: var(--hacker-accent);
text-shadow: 0 0 5px var(--hacker-accent);
}
.blink {
animation: blink 1s step-end infinite;
}
@keyframes blink {
from, to { opacity: 1; }
50% { opacity: 0; }
}
.glow-text {
text-shadow: 0 0 5px currentColor;
}
.glow-box {
box-shadow: 0 0 10px currentColor;
}
.hacker-divider {
height: 1px;
background: linear-gradient(90deg, transparent 0%, var(--hacker-border) 50%, transparent 100%);
margin: 1rem 0;
}
body::-webkit-scrollbar {
width: 8px;
background-color: rgba(0, 50, 100, 0.3);
}
body::-webkit-scrollbar-thumb {
background: var(--hacker-primary);
border-radius: 4px;
box-shadow: inset 0 0 5px rgba(0, 200, 255, 0.5);
}
/* レイヤーエディターのスタイル */
#layer-editor {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 20, 40, 0.97);
border: 2px solid var(--hacker-border);
box-shadow: 0 0 30px rgba(0, 200, 255, 0.5);
z-index: 2000;
width: 90%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
padding: 1.5rem;
font-size: 0.95rem;
}
#layer-editor h3 {
color: var(--hacker-primary);
text-shadow: 0 0 5px var(--hacker-primary);
border-bottom: 2px solid var(--hacker-border);
padding-bottom: 0.5rem;
margin-bottom: 1.5rem;
font-weight: 700;
}
.layer-tabs {
display: flex;
margin-bottom: 1.5rem;
border-bottom: 2px solid var(--hacker-border);
}
.layer-tab {
padding: 0.5rem 1.2rem;
cursor: pointer;
border: 2px solid transparent;
margin-right: 0.5rem;
border-radius: 4px 4px 0 0;
transition: all 0.2s ease;
background: rgba(0, 50, 100, 0.3);
font-size: 0.9rem;
}
.layer-tab:hover {
background: rgba(0, 100, 200, 0.3);
}
.layer-tab.active {
background: rgba(0, 100, 200, 0.6);
border-color: var(--hacker-border);
border-bottom-color: rgba(0, 20, 40, 0.97);
margin-bottom: -2px;
}
.layer-tab-content {
padding: 1rem 0;
display: none;
}
.layer-tab-content.active {
display: block;
}
.layer-form-group {
margin-bottom: 1.2rem;
}
.layer-form-group label {
display: block;
margin-bottom: 0.5rem;
color: var(--hacker-secondary);
font-weight: bold;
font-size: 0.9rem;
}
.layer-form-group input,
.layer-form-group textarea,
.layer-form-group select {
width: 100%;
padding: 0.6rem;
background: rgba(0, 10, 20, 0.9);
border: 1px solid var(--hacker-border);
color: var(--hacker-text);
font-family: 'Source Code Pro', monospace;
transition: all 0.3s ease;
font-size: 0.9rem;
}
.layer-form-group textarea {
min-height: 100px;
resize: vertical;
}
.layer-form-group input:focus,
.layer-form-group textarea:focus,
.layer-form-group select:focus {
outline: none;
border-color: var(--hacker-primary);
box-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
}
.layer-form-actions {
margin-top: 1.5rem;
display: flex;
gap: 0.8rem;
}
.layer-form-actions button {
flex: 1;
padding: 0.75rem;
font-size: 0.9rem;
}
/* レイヤーツリーのスタイル */
#layer-tree {
display: none;
position: absolute;
top: 10px;
left: 10px;
background: rgba(0, 20, 40, 0.95);
border: 2px solid var(--hacker-border);
box-shadow: 0 0 20px rgba(0, 200, 255, 0.4);
z-index: 1000;
width: 300px;
max-height: 80vh;
overflow-y: auto;
padding: 1rem;
font-size: 0.9rem;
}
#layer-tree h3 {
color: var(--hacker-primary);
margin-bottom: 1rem;
font-size: 1.1rem;
border-bottom: 1px solid var(--hacker-border);
padding-bottom: 0.5rem;
}
.layer-tree-item {
padding: 0.5rem;
cursor: pointer;
display: flex;
align-items: center;
transition: all 0.2s ease;
border-bottom: 1px solid rgba(0, 66, 133, 0.3);
}
.layer-tree-item:hover {
background: rgba(0, 50, 100, 0.3);
}
.layer-tree-item.selected {
background: rgba(0, 100, 200, 0.3);
color: var(--hacker-primary);
}
.layer-tree-toggle {
margin-right: 0.5rem;
width: 1em;
display: inline-block;
text-align: center;
}
.layer-count {
margin-left: auto;
font-size: 0.8em;
color: var(--hacker-secondary);
opacity: 0.7;
}
.layer-tree-icon {
margin-right: 0.8rem;
font-size: 1.1em;
width: 1.2em;
text-align: center;
}
.layer-name {
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.layer-tree-buttons {
margin-left: auto;
display: flex;
gap: 0.3rem;
}
.layer-tree-btn {
background: none;
border: none;
color: var(--hacker-text);
cursor: pointer;
padding: 0.2rem;
font-size: 0.9em;
opacity: 0.7;
transition: all 0.2s ease;
}
.layer-tree-btn:hover {
color: var(--hacker-primary);
opacity: 1;
transform: scale(1.1);
}
.layer-tree-item-group {
margin-left: 1.5rem;
display: none;
border-left: 1px dashed var(--hacker-border);
padding-left: 0.8rem;
}
.layer-tree-item-group.expanded {
display: block;
}
/* ギャラリー用スタイル */
#gallery-container {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 10, 20, 0.95);
z-index: 2000;
overflow-y: auto;
padding: 2rem;
}
.gallery-map-item {
background: rgba(0, 20, 40, 0.8);
border: 1px solid var(--hacker-border);
padding: 1rem;
margin-bottom: 1rem;
transition: all 0.3s ease;
}
.gallery-map-item:hover {
background: rgba(0, 40, 80, 0.8);
box-shadow: 0 0 15px rgba(0, 200, 255, 0.4);
}
.gallery-map-title {
color: var(--hacker-primary);
font-weight: bold;
margin-bottom: 0.5rem;
cursor: pointer;
padding: 0.25rem;
border-radius: 3px;
}
.gallery-map-title:hover {
background: rgba(0, 100, 200, 0.3);
}
.gallery-map-title.editing {
background: rgba(0, 100, 200, 0.5);
outline: 1px solid var(--hacker-primary);
}
.gallery-map-preview {
height: 150px;
background-color: rgba(0, 30, 60, 0.5);
margin-bottom: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
color: var(--hacker-secondary);
font-size: 0.9rem;
white-space: pre-wrap;
line-height: 1.4;
overflow: hidden;
}
.gallery-map-actions {
display: flex;
gap: 0.5rem;
}
.gallery-btn {
flex: 1;
padding: 0.5rem;
font-size: 0.8rem;
}
/* プラグインマネージャー */
#plugin-manager {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 20, 40, 0.95);
border: 2px solid var(--hacker-border);
padding: 2rem;
z-index: 2002;
width: 90%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
}
#plugin-manager h3 {
color: var(--hacker-primary);
margin-bottom: 1.5rem;
text-align: center;
border-bottom: 1px solid var(--hacker-border);
padding-bottom: 0.5rem;
}
#plugin-url {
width: 100%;
margin-bottom: 1rem;
background: rgba(0, 10, 20, 0.8);
border: 1px solid var(--hacker-border);
color: var(--hacker-text);
padding: 0.75rem;
font-family: 'Source Code Pro', monospace;
}
#plugin-list {
max-height: 300px;
overflow-y: auto;
margin: 1.5rem 0;
padding: 0.5rem;
background: rgba(0, 10, 20, 0.5);
border: 1px solid var(--hacker-border);
}
.plugin-item {
padding: 0.8rem;
margin-bottom: 0.8rem;
background: rgba(0, 30, 60, 0.5);
border-left: 3px solid var(--hacker-primary);
}
.plugin-item-actions {
display: flex;
gap: 0.5rem;
margin-top: 0.8rem;
}
.plugin-item-actions button {
flex: 1;
padding: 0.4rem;
font-size: 0.8rem;
}
/* レスポンシブ対応 */
@media (max-width: 768px) {
#layer-editor, #plugin-manager {
width: 95%;
padding: 1rem;
}
.layer-tabs {
flex-wrap: wrap;
}
.layer-tab {
margin-bottom: 0.5rem;
}
#layer-tree {
width: 250px;
}
.hacker-btn {
padding: 0.5rem 1rem;
font-size: 0.8rem;
margin: 0.3rem;
}
}
</style>
</head>
<body>
<div id="loading">
<div class="loader"></div>
<div class="terminal-line glow-text">INITIALIZING MAP SYSTEM<span class="blink">_</span></div>
<div class="terminal-line glow-text">LOADING ASSETS...</div>
<div class="terminal-line glow-text">CONNECTING TO DATABASE...</div>
</div>
<div class="hacker-header">
<h1 class="text-3xl font-bold text-center glow-text" style="color: var(--hacker-primary);">MAP EDITOR <span class="text-sm"></span></h1>
<p class="text-center text-sm mt-2 glow-text" style="color: var(--hacker-secondary);">FOR SCHOOL</p>
</div>
<div class="container mx-auto px-4">
<div class="flex flex-wrap mb-4">
<button id="edit-next-marker" class="hacker-btn secondary disabled">
<span class="glow-text">次のマーカーを編集</span>
</button>
<button id="save-map-btn" class="hacker-btn secondary">
<span class="glow-text">マップを保存</span>
</button>
<button id="load-map-btn" class="hacker-btn secondary">
<span class="glow-text">マップを読み込み</span>
</button>
<button id="replace-current-map-btn" class="hacker-btn secondary" style="display: none;">
<span class="glow-text"><span id="replace-map-name"></span>を置き換えて保存</span>
</button>
<button id="replace-other-map-btn" class="hacker-btn secondary">
<span class="glow-text">他のマップを置き換えて保存</span>
</button>
<button onclick="if(confirm('現在のマップのすべてのデータが消去されます。いいですか?')){clearCurrentMap()}" class="hacker-btn danger">
<span class="glow-text">現在のマップをリセット</span>
</button>
<button id="add-layer-btn" class="hacker-btn">
<span class="glow-text">レイヤーを追加</span>
</button>
<button id="manage-plugins-btn" class="hacker-btn">
<span class="glow-text">プラグイン管理</span>
</button>
<button id="toggle-layer-tree-btn" class="hacker-btn secondary">
<span class="glow-text">レイヤーツリー</span>
</button>
</div>
<div class="hacker-container">
<div class="terminal-line glow-text">マップエディター:</div>
<div id="map"></div>
</div>
<!-- マーカーエディタ -->
<div id="marker-editor">
<h3>MARKER EDITOR</h3>
<div class="terminal-line">緯度:</div>
<input type="text" id="marker-lat" placeholder="35.681236">
<div class="hacker-divider"></div>
<div class="terminal-line">経度:</div>
<input type="text" id="marker-lng" placeholder="139.767125">
<div class="hacker-divider"></div>
<div class="terminal-line">アイコンのソース:</div>
<div id="icon-upload-input" style="display: block; margin-bottom: 20px;">
<label for="marker-icon-upload">UPLOAD ICON:</label>
<input type="file" id="marker-icon-upload" accept="image/*">
</div>
<div class="hacker-divider"></div>
<div id="icon-url-input" style="display: block; margin-bottom: 20px;">
<label for="marker-icon-url">アイコンのURL:</label>
<input type="text" id="marker-icon-url" value="https://unpkg.com/leaflet@1.9.3/dist/images/marker-icon-2x.png">
<button id="load-icon-url" class="hacker-btn secondary mt-2">LOAD IMAGE</button>
</div>
<img id="icon-preview" src="" alt="ICON PREVIEW">
<div id="icon-settings">
<div class="terminal-line">アイコンの幅:</div>
<input type="range" id="icon-width" min="10" max="100" value="25">
<input type="number" id="icon-width-input" min="10" max="100" value="25">
<span id="icon-width-value" style="color: var(--hacker-primary);">25</span>px
<div class="terminal-line">アイコンの高さ:</div>
<input type="range" id="icon-height" min="10" max="100" value="41">
<input type="number" id="icon-height-input" min="10" max="100" value="41">
<span id="icon-height-value" style="color: var(--hacker-primary);">41</span>px
</div>
<div class="hacker-divider"></div>
<div class="terminal-line">ポップアップHTML:</div>
<textarea id="marker-popup" placeholder="<b>LOCATION NAME</b><br>Additional info here"></textarea>
<div class="terminal-line">ツールチップHTML:</div>
<textarea id="marker-tooltip" placeholder="Hover text here"></textarea>
<div class="hacker-divider"></div>
<button id="save-marker" class="hacker-btn">
<span class="glow-text">保存</span>
</button>
<button id="delete-marker" class="hacker-btn danger">
<span class="glow-text">削除</span>
</button>
</div>
<!-- レイヤーエディタ -->
<div id="layer-editor">
<h3>LAYER EDITOR</h3>
<div class="layer-tabs">
<div class="layer-tab active" data-tab="base">ベースレイヤー</div>
<div class="layer-tab" data-tab="overlay">オーバーレイ</div>
<div class="layer-tab" data-tab="other">その他</div>
</div>
<!-- ベースレイヤータブ -->
<div class="layer-tab-content active" id="base-tab">
<div class="layer-form-group">
<label for="base-layer-type">レイヤータイプ:</label>
<select id="base-layer-type">
<option value="tile">タイルレイヤー (TileLayer)</option>
<option value="canvas">Canvasレイヤー (L.Canvas)</option>
<option value="svg">SVGレイヤー (L.SVG)</option>
<option value="grid">グリッドレイヤー (L.GridLayer)</option>
</select>
</div>
<div class="layer-form-group" id="tile-url-group">
<label for="tile-layer-url">タイルURL:</label>
<input type="text" id="tile-layer-url" value="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png">
</div>
<div class="layer-form-group" id="tile-attribution-group">
<label for="tile-layer-attribution">クレジット表示:</label>
<input type="text" id="tile-layer-attribution" value="© OpenStreetMap contributors">
</div>
<div class="layer-form-group" id="tile-options-group">
<label for="tile-layer-options">オプション (JSON):</label>
<textarea id="tile-layer-options" placeholder='{"minZoom": 0, "maxZoom": 19, "subdomains": "abc"}'></textarea>
</div>
<div class="layer-form-actions">
<button id="add-base-layer" class="hacker-btn">
<span class="glow-text">レイヤーを追加</span>
</button>
<button id="cancel-layer" class="hacker-btn danger">
<span class="glow-text">キャンセル</span>
</button>
</div>
</div>
<!-- オーバーレイタブ -->
<div class="layer-tab-content" id="overlay-tab">
<div class="layer-form-group">
<label for="overlay-layer-type">レイヤータイプ:</label>
<select id="overlay-layer-type">
<option value="marker">マーカー (Marker)</option>
<option value="polyline">ポリライン (Polyline)</option>
<option value="polygon">ポリゴン (Polygon)</option>
<option value="circle">サークル (Circle)</option>
<option value="circlemarker">サークルマーカー (CircleMarker)</option>
<option value="geojson">GeoJSONレイヤー (GeoJSON)</option>
<option value="image">画像オーバーレイ (ImageOverlay)</option>
<option value="video">ビデオオーバーレイ (VideoOverlay)</option>
</select>
</div>
<div class="layer-form-group" id="overlay-options-group">
<label for="overlayer-options">オプション (JSON):</label>
<textarea id="overlayer-options" placeholder='{"color": "#ff0000", "weight": 5}'></textarea>
</div>
<div class="layer-form-group" id="overlay-coords-group">
<label>座標 (クリックで追加):</label>
<div id="overlay-coords-list"></div>
</div>
<div class="layer-form-actions">
<button id="add-overlay-layer" class="hacker-btn">
<span class="glow-text">レイヤーを追加</span>
</button>
<button id="cancel-overlay-layer" class="hacker-btn danger">
<span class="glow-text">キャンセル</span>
</button>
</div>
</div>
<!-- その他タブ -->
<div class="layer-tab-content" id="other-tab">
<div class="layer-form-group">
<label for="other-layer-type">レイヤータイプ:</label>
<select id="other-layer-type">
<option value="layergroup">レイヤーグループ (LayerGroup)</option>
<option value="featuregroup">フィーチャーグループ (FeatureGroup)</option>
<option value="control">レイヤー切替UI (Control.Layers)</option>
<option value="heatmap">ヒートマップレイヤー (Heatmap)</option>
<option value="cluster">クラスターレイヤー (MarkerCluster)</option>
<option value="vectorgrid">ベクターグリッド (VectorGrid)</option>
<option value="custom">カスタムレイヤー</option>
</select>
</div>
<div class="layer-form-group" id="other-options-group">
<label for="other-layer-options">オプション (JSON):</label>
<textarea id="other-layer-options" placeholder='{"radius": 25, "maxZoom": 18}'></textarea>
</div>
<div class="layer-form-group" id="other-custom-code-group">
<label for="other-layer-custom-code">カスタムコード:</label>
<textarea id="other-layer-custom-code" placeholder="function(layer) { /* カスタム処理 */ }"></textarea>
</div>
<div class="layer-form-actions">
<button id="add-other-layer" class="hacker-btn">
<span class="glow-text">レイヤーを追加</span>
</button>
<button id="cancel-other-layer" class="hacker-btn danger">
<span class="glow-text">キャンセル</span>
</button>
</div>
</div>
</div>
<div class="hacker-container">
<button id="generate-html" class="hacker-btn">
<span class="glow-text">HTMLを生成</span>
</button>
<button id="copyButton" class="hacker-btn secondary">
<span class="glow-text">COPY</span>
</button>
<div class="terminal-line glow-text">埋め込みコード:</div>
<div id="output-html"></div>
</div>
</div>
<!-- マップ保存モーダル -->
<div id="save-map-modal">
<h3>マップを保存</h3>
<span>すでにあるマップの名前を入力すると、そのマップを置き換えます。</span>
<input type="text" id="save-map-name" placeholder="マップ名を入力">
<button id="confirm-save-map" class="hacker-btn">
<span class="glow-text">保存</span>
</button>
<button id="cancel-save-map" class="hacker-btn danger">
<span class="glow-text">キャンセル</span>
</button>
</div>
<!-- マップギャラリー -->
<div id="gallery-container">
<div class="container mx-auto">
<h2 class="hacker-title text-center">保存されたマップ</h2>
<div id="gallery-map-list" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"></div>
<div class="text-center mt-4">
<button id="close-gallery" class="hacker-btn danger">
<span class="glow-text">閉じる</span>
</button>
</div>
</div>
</div>
<!-- プラグインマネージャー -->
<div id="plugin-manager">
<h3>プラグイン管理</h3>
<div class="layer-form-group">
<label for="plugin-url">プラグインURL:</label>
<input type="text" id="plugin-url" placeholder="https://unpkg.com/leaflet.markercluster@1.4.1/dist/leaflet.markercluster.js">
</div>
<div class="layer-form-actions">
<button id="add-plugin" class="hacker-btn">
<span class="glow-text">プラグインを追加</span>
</button>
<button id="close-plugin-manager" class="hacker-btn danger">
<span class="glow-text">閉じる</span>
</button>
</div>
<div id="plugin-list"></div>
</div>
<!-- レイヤーツリー -->
<div id="layer-tree" style="display: none;">
<h3>レイヤーツリー</h3>
<div id="layer-tree-content"></div>
</div>
<script src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/styles/mono-blue.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/highlight.min.js"></script>
<script>
// グローバル変数
let map;
let editingMarker = null;
let hoveredMarker = null;
let nextMarkerEdit = false;
let markers = [];
let currentMapName = '';
let layers = [];
let plugins = [];
let layerControls = {};
let currentEditingLayer = null;
let overlayCoords = [];
// 初期化処理
window.onload = function() {
const loading = document.getElementById('loading');
loading.style.opacity = '0';
setTimeout(() => {
loading.style.display = 'none';
initMap();
updateEditNextMarkerButton();
loadPluginsFromStorage();
}, 1000);
// イベントリスナーの設定
setupEventListeners();
};
// マップ初期化
function initMap() {
map = L.map("map").setView([33.321797711641395, 130.52061378343208], 16);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap contributors'
}).addTo(map);
// マーカーイベントの設定
map.on("click", function(e) {
if (nextMarkerEdit) {
return;
}
if (currentEditingLayer) {
handleLayerEditingClick(e);
return;
}
if (editingMarker) {
const latlng = e.latlng;
editingMarker.setLatLng([latlng.lat, latlng.lng]);
document.getElementById("marker-lat").value = latlng.lat;
document.getElementById("marker-lng").value = latlng.lng;
updatePreviewSize();
saveCurrentMapToStorage();
} else {
const latlng = e.latlng;
const marker = L.marker(latlng).addTo(map);
marker.bindPopup("新しいマーカーのポップアップ");
marker.bindTooltip("新しいマーカーのツールチップ");
marker.on("mouseover", function() {
hoveredMarker = marker;
});
marker.on("mouseout", function() {
if (hoveredMarker === marker) {
hoveredMarker = null;
}
});
document.getElementById("marker-icon-url").value = "https://unpkg.com/leaflet@1.9.3/dist/images/marker-icon-2x.png";
openEditor(marker);
saveCurrentMapToStorage();
}
});
// マーカーが変更されたらボタンの状態を更新
map.on('layeradd layerremove', function() {
updateEditNextMarkerButton();
updateLayerTree();
});
// ポップアップが開いた時の処理をここに移動
map.on('popupopen', function(e) {
const marker = e.popup._source;
if (nextMarkerEdit) {
openEditor(marker);
nextMarkerEdit = false;
document.getElementById("edit-next-marker").textContent = "次のマーカーを編集";
document.getElementById("edit-next-marker").classList.remove("danger");
document.getElementById("edit-next-marker").classList.add("secondary");
}
});
}
// レイヤー編集時のクリック処理
function handleLayerEditingClick(e) {
const latlng = e.latlng;
overlayCoords.push([latlng.lat, latlng.lng]);
updateOverlayCoordsList();
// 現在編集中のレイヤーを更新
if (currentEditingLayer) {
const layerType = document.getElementById("overlay-layer-type").value;
if (layerType === 'polyline' || layerType === 'polygon') {
if (currentEditingLayer.setLatLngs) {
currentEditingLayer.setLatLngs(overlayCoords);
}
} else if (layerType === 'circle' || layerType === 'circlemarker') {
if (currentEditingLayer.setLatLng) {
currentEditingLayer.setLatLng(latlng);
}
}
}
}
// オーバーレイ座標リストを更新
function updateOverlayCoordsList() {
const coordsList = document.getElementById("overlay-coords-list");
coordsList.innerHTML = '';
overlayCoords.forEach((coord, index) => {
const coordItem = document.createElement('div');
coordItem.className = 'terminal-line';
coordItem.textContent = `${index + 1}. ${coord[0].toFixed(6)}, ${coord[1].toFixed(6)}`;
coordsList.appendChild(coordItem);
});
}
// イベントリスナーの設定
function setupEventListeners() {
// マーカー編集関連
document.getElementById("save-marker").addEventListener("click", saveMarker);
document.getElementById("delete-marker").addEventListener("click", deleteMarker);
document.addEventListener("keydown", function(e) {
if (e.key === "e" && hoveredMarker) {
openEditor(hoveredMarker);
}
});
// アイコン設定関連
document.getElementById("marker-icon-upload").addEventListener("change", handleIconUpload);
document.getElementById("load-icon-url").addEventListener("click", loadIconFromUrl);
document.getElementById("icon-width").addEventListener("input", syncWidth);
document.getElementById("icon-width-input").addEventListener("input", syncWidth);
document.getElementById("icon-height").addEventListener("input", syncHeight);
document.getElementById("icon-height-input").addEventListener("input", syncHeight);
// マーカー編集モード関連
document.getElementById("edit-next-marker").addEventListener("click", toggleEditNextMarkerMode);
// マップ保存/読み込み関連
document.getElementById("save-map-btn").addEventListener("click", showSaveMapModal);
document.getElementById("load-map-btn").addEventListener("click", showGallery);
document.getElementById("confirm-save-map").addEventListener("click", saveCurrentMapWithName);
document.getElementById("cancel-save-map").addEventListener("click", hideSaveMapModal);
document.getElementById("close-gallery").addEventListener("click", hideGallery);
// マーカーエディタのドラッグ移動
setupEditorDrag();
// HTML生成関連
document.getElementById("generate-html").addEventListener("click", generateMapHTML);
document.getElementById("copyButton").onclick = copyHTMLToClipboard;
// レイヤー関連
document.getElementById("add-layer-btn").addEventListener("click", showLayerEditor);
document.getElementById("cancel-layer").addEventListener("click", hideLayerEditor);
document.getElementById("add-base-layer").addEventListener("click", addBaseLayer);
document.getElementById("add-overlay-layer").addEventListener("click", addOverlayLayer);
document.getElementById("cancel-overlay-layer").addEventListener("click", cancelOverlayLayer);
document.getElementById("add-other-layer").addEventListener("click", addOtherLayer);
document.getElementById("cancel-other-layer").addEventListener("click", cancelOtherLayer);
document.getElementById("toggle-layer-tree-btn").addEventListener("click", toggleLayerTree);
// タブ切り替え
document.querySelectorAll('.layer-tab').forEach(tab => {
tab.addEventListener('click', function() {
const tabId = this.dataset.tab;
switchLayerTab(tabId);
});
});
// レイヤータイプ変更
document.getElementById("base-layer-type").addEventListener("change", updateBaseLayerForm);
document.getElementById("overlay-layer-type").addEventListener("change", updateOverlayLayerForm);
document.getElementById("other-layer-type").addEventListener("change", updateOtherLayerForm);
// プラグイン管理
document.getElementById("manage-plugins-btn").addEventListener("click", showPluginManager);
document.getElementById("close-plugin-manager").addEventListener("click", hidePluginManager);
document.getElementById("add-plugin").addEventListener("click", addPlugin);
}
// レイヤーエディター表示関数を改善
function showLayerEditor() {
const editor = document.getElementById("layer-editor");
editor.style.display = "block";
// 画面中央に表示
editor.style.left = "50%";
editor.style.top = "50%";
editor.style.transform = "translate(-50%, -50%)";
// タブをリセット
switchLayerTab('base');
updateBaseLayerForm();
updateOverlayLayerForm();
updateOtherLayerForm();
// フォーカスを設定
setTimeout(() => {
const firstInput = editor.querySelector('input, select, textarea');
if (firstInput) firstInput.focus();
}, 100);
}
// タブ切り替え関数を改善
function switchLayerTab(tabId) {
// タブを非アクティブ化
document.querySelectorAll('.layer-tab').forEach(tab => {
tab.classList.remove('active');
});
// タブコンテンツを非表示
document.querySelectorAll('.layer-tab-content').forEach(content => {
content.classList.remove('active');
});
// 選択されたタブをアクティブ化
const tab = document.querySelector(`.layer-tab[data-tab="${tabId}"]`);
if (tab) {
tab.classList.add('active');
document.getElementById(`${tabId}-tab`).classList.add('active');
// 現在編集中のレイヤーをクリア
if (currentEditingLayer) {
map.removeLayer(currentEditingLayer);
currentEditingLayer = null;
overlayCoords = [];
updateOverlayCoordsList();
}
}
}
// レイヤータブを切り替え
function switchLayerTab(tabId) {
// タブを非アクティブ化
document.querySelectorAll('.layer-tab').forEach(tab => {
tab.classList.remove('active');
});
// タブコンテンツを非表示
document.querySelectorAll('.layer-tab-content').forEach(content => {
content.classList.remove('active');
});
// 選択されたタブをアクティブ化
document.querySelector(`.layer-tab[data-tab="${tabId}"]`).classList.add('active');
document.getElementById(`${tabId}-tab`).classList.add('active');
}
// ベースレイヤーフォームを更新
function updateBaseLayerForm() {
const layerType = document.getElementById("base-layer-type").value;
// すべてのグループを非表示
document.getElementById("tile-url-group").style.display = 'none';
document.getElementById("tile-attribution-group").style.display = 'none';
document.getElementById("tile-options-group").style.display = 'none';
// 選択されたタイプに応じて表示
if (layerType === 'tile') {
document.getElementById("tile-url-group").style.display = 'block';
document.getElementById("tile-attribution-group").style.display = 'block';
document.getElementById("tile-options-group").style.display = 'block';
}
}
// オーバーレイレイヤーフォーム更新時にプレビューを強化
function updateOverlayLayerForm() {
const layerType = document.getElementById("overlay-layer-type").value;
let options = {};
try {
const optionsText = document.getElementById("overlayer-options").value;
if (optionsText) {
options = JSON.parse(optionsText);
}
} catch (e) {
console.error("Options JSON error:", e);
}
// 現在編集中のレイヤーをクリア
if (currentEditingLayer) {
map.removeLayer(currentEditingLayer);
currentEditingLayer = null;
}
overlayCoords = [];
updateOverlayCoordsList();
// オプションフォームのプレースホルダーを設定
let placeholder = '{"color": "#ff0000", "weight": 5}';
// 新しいレイヤーを作成
switch(layerType) {
case 'polyline':
currentEditingLayer = L.polyline([], options).addTo(map);
break;
case 'polygon':
currentEditingLayer = L.polygon([], options).addTo(map);
placeholder = '{"color": "#ff0000", "fillColor": "#ff0000", "weight": 5}';
break;
case 'circle':
currentEditingLayer = L.circle([0, 0], options).addTo(map);
placeholder = '{"radius": 500, "color": "#ff0000", "fillColor": "#ff0000"}';
break;
case 'circlemarker':
currentEditingLayer = L.circleMarker([0, 0], options).addTo(map);
placeholder = '{"radius": 10, "color": "#ff0000", "fillColor": "#ff0000"}';
break;
case 'marker':
currentEditingLayer = L.marker([0, 0], options).addTo(map);
placeholder = '{"draggable": true}';
break;
}
document.getElementById("overlayer-options").placeholder = placeholder;
// オプション変更時のリアルタイム更新
document.getElementById("overlayer-options").addEventListener('input', function() {
try {
const newOptions = JSON.parse(this.value);
if (currentEditingLayer && currentEditingLayer.setStyle) {
currentEditingLayer.setStyle(newOptions);
}
} catch (e) {
// JSONが無効な場合は無視
}
});
}
// その他レイヤーフォームを更新
function updateOtherLayerForm() {
const layerType = document.getElementById("other-layer-type").value;
document.getElementById("other-custom-code-group").style.display = 'none';
if (layerType === 'custom') {
document.getElementById("other-custom-code-group").style.display = 'block';
}
}
// ベースレイヤー追加関数にバリデーションを追加
function addBaseLayer() {
const layerType = document.getElementById("base-layer-type").value;
const layerName = prompt("レイヤー名を入力してください", "新しいレイヤー");
if (!layerName) return;
let layer;
let options = {};
try {
const optionsText = document.getElementById("tile-layer-options").value;
if (optionsText) {
options = JSON.parse(optionsText);
}
} catch (e) {
alert("オプションのJSONが不正です:\n" + e.message);
return;
}
if (layerType === 'tile') {
const url = document.getElementById("tile-layer-url").value;
const attribution = document.getElementById("tile-layer-attribution").value;
if (!url) {
alert("タイルURLを入力してください");
return;
}
// URLバリデーション
if (!url.includes('{z}') || !url.includes('{x}') || !url.includes('{y}')) {
if (!confirm("タイルURLに{z}, {x}, {y}のプレースホルダーが含まれていません。続行しますか?")) {
return;
}
}
layer = L.tileLayer(url, {
attribution: attribution,
...options
}).addTo(map);
}
else if (layerType === 'canvas') {
layer = L.canvas(options).addTo(map);
} else if (layerType === 'svg') {
layer = L.svg(options).addTo(map);
} else if (layerType === 'grid') {
layer = L.gridLayer(options).addTo(map);
}
if (layer) {
layers.push({
id: 'layer-' + Date.now(),
name: layerName,
type: layerType,
layer: layer,
options: options
});
saveCurrentMapToStorage();
updateLayerTree();
hideLayerEditor();
alert(`レイヤー「${layerName}」を追加しました`);
}
}
// オーバーレイレイヤーを追加
function addOverlayLayer() {
const layerType = document.getElementById("overlay-layer-type").value;
const layerName = prompt("レイヤー名を入力してください", "新しいオーバーレイ");
if (!layerName) return;
let layer;
let options = {};
try {
const optionsText = document.getElementById("overlayer-options").value;
if (optionsText) {
options = JSON.parse(optionsText);
}
} catch (e) {
alert("オプションのJSONが不正です");
return;
}
if (layerType === 'polyline') {
if (overlayCoords.length < 2) {
alert("ポリラインには少なくとも2点の座標が必要です");
return;
}
layer = L.polyline(overlayCoords, options).addTo(map);
} else if (layerType === 'polygon') {
if (overlayCoords.length < 3) {
alert("ポリゴンには少なくとも3点の座標が必要です");
return;
}
layer = L.polygon(overlayCoords, options).addTo(map);
} else if (layerType === 'circle') {
if (overlayCoords.length === 0) {
alert("サークルには中心点が必要です");
return;
}
layer = L.circle(overlayCoords[0], options).addTo(map);
} else if (layerType === 'circlemarker') {
if (overlayCoords.length === 0) {
alert("サークルマーカーには中心点が必要です");
return;
}
layer = L.circleMarker(overlayCoords[0], options).addTo(map);
} else if (layerType === 'marker') {
if (overlayCoords.length === 0) {
alert("マーカーには位置が必要です");
return;
}
layer = L.marker(overlayCoords[0], options).addTo(map);
}
if (layer) {
layers.push({
id: 'layer-' + Date.now(),
name: layerName,
type: layerType,
layer: layer,
options: options,
coords: [...overlayCoords]
});
saveCurrentMapToStorage();
updateLayerTree();
hideLayerEditor();
}
}
// レイヤーエディタを非表示
function hideLayerEditor() {
document.getElementById("layer-editor").style.display = "none";
if (currentEditingLayer) {
map.removeLayer(currentEditingLayer);
currentEditingLayer = null;
overlayCoords = [];
}
}
// オーバーレイレイヤー追加をキャンセル
function cancelOverlayLayer() {
if (currentEditingLayer) {
map.removeLayer(currentEditingLayer);
currentEditingLayer = null;
}
overlayCoords = [];
hideLayerEditor();
}
// その他レイヤーを追加
function addOtherLayer() {
const layerType = document.getElementById("other-layer-type").value;
const layerName = prompt("レイヤー名を入力してください", "新しいレイヤー");
if (!layerName) return;
let layer;
let options = {};
try {
const optionsText = document.getElementById("other-layer-options").value;
if (optionsText) {
options = JSON.parse(optionsText);
}
} catch (e) {
alert("オプションのJSONが不正です");
return;
}
if (layerType === 'layergroup') {
layer = L.layerGroup().addTo(map);
} else if (layerType === 'featuregroup') {
layer = L.featureGroup().addTo(map);
} else if (layerType === 'control') {
// ベースレイヤーとオーバーレイレイヤーを収集
const baseLayers = {};
const overlays = {};
layers.forEach(l => {
if (l.type === 'tile' || l.type === 'canvas' || l.type === 'svg' || l.type === 'grid') {
baseLayers[l.name] = l.layer;
} else {
overlays[l.name] = l.layer;
}
});
layer = L.control.layers(baseLayers, overlays, options).addTo(map);
layerControls[layer._leaflet_id] = layer;
} else if (layerType === 'heatmap') {
// ヒートマッププラグインが読み込まれているか確認
if (typeof L.HeatLayer === 'undefined') {
alert("ヒートマッププラグインが読み込まれていません");
return;
}
layer = L.heatLayer([], options).addTo(map);
} else if (layerType === 'cluster') {
// クラスタープラグインが読み込まれているか確認
if (typeof L.markerClusterGroup === 'undefined') {
alert("クラスタープラグインが読み込まれていません");
return;
}
layer = L.markerClusterGroup(options).addTo(map);
} else if (layerType === 'custom') {
const customCode = document.getElementById("other-layer-custom-code").value;
try {
// カスタムコードを実行
const customFunc = new Function('layer', 'map', customCode);
layer = customFunc(L, map);
if (!layer) {
alert("カスタムコードはレイヤーオブジェクトを返す必要があります");
return;
}
layer.addTo(map);
} catch (e) {
alert("カスタムコードの実行中にエラーが発生しました: " + e.message);
return;
}
}
if (layer) {
layers.push({
id: 'layer-' + Date.now(),
name: layerName,
type: layerType,
layer: layer,
options: options
});
saveCurrentMapToStorage();
updateLayerTree();
hideLayerEditor();
}
}
// その他レイヤー追加をキャンセル
function cancelOtherLayer() {
hideLayerEditor();
}
// レイヤーツリーを表示/非表示
function toggleLayerTree() {
const layerTree = document.getElementById("layer-tree");
if (layerTree.style.display === 'none') {
layerTree.style.display = 'block';
updateLayerTree();
} else {
layerTree.style.display = 'none';
}
}
// レイヤーツリーを更新
// レイヤーツリーの改善
function updateLayerTree() {
const layerTreeContent = document.getElementById("layer-tree-content");
layerTreeContent.innerHTML = '';
// レイヤーをタイプ別に分類
const layerTypes = {
'base': ['tile', 'canvas', 'svg', 'grid'],
'overlay': ['marker', 'polyline', 'polygon', 'circle', 'circlemarker', 'geojson', 'image', 'video'],
'other': ['layergroup', 'featuregroup', 'control', 'heatmap', 'cluster', 'vectorgrid', 'custom']
};
// 各タイプごとに表示
Object.entries(layerTypes).forEach(([typeName, typeList]) => {
const typeLayers = layers.filter(l => typeList.includes(l.type));
if (typeLayers.length > 0) {
// タイプヘッダー
const header = document.createElement('div');
header.className = 'layer-tree-item';
header.innerHTML = `
<span class="layer-tree-toggle"></span>
${typeName === 'base' ? 'ベースレイヤー' :
typeName === 'overlay' ? 'オーバーレイレイヤー' : 'その他レイヤー'}
<span class="layer-count">(${typeLayers.length})</span>
`;
const groupId = `${typeName}-layers-group`;
header.addEventListener('click', function() {
this.querySelector('.layer-tree-toggle').textContent =
this.querySelector('.layer-tree-toggle').textContent === '▸' ? '▾' : '▸';
document.getElementById(groupId).classList.toggle('expanded');
});
layerTreeContent.appendChild(header);
// レイヤーグループ
const group = document.createElement('div');
group.id = groupId;
group.className = 'layer-tree-item-group';
typeLayers.forEach(layer => {
const layerItem = document.createElement('div');
layerItem.className = 'layer-tree-item';
// レイヤーアイコン
const icon = document.createElement('span');
icon.className = 'layer-tree-icon';
icon.innerHTML = getLayerIcon(layer.type);
layerItem.appendChild(icon);
// レイヤー名
const nameSpan = document.createElement('span');
nameSpan.textContent = layer.name;
nameSpan.className = 'layer-name';
layerItem.appendChild(nameSpan);
// 操作ボタン
const btnGroup = document.createElement('div');
btnGroup.className = 'layer-tree-buttons';
const editBtn = document.createElement('button');
editBtn.className = 'layer-tree-btn edit-btn';
editBtn.title = '編集';
editBtn.innerHTML = '✏️';
editBtn.addEventListener('click', (e) => {
e.stopPropagation();
editLayer(layer);
});
const deleteBtn = document.createElement('button');
deleteBtn.className = 'layer-tree-btn delete-btn';
deleteBtn.title = '削除';
deleteBtn.innerHTML = '🗑️';
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
deleteLayer(layer);
});
btnGroup.appendChild(editBtn);
btnGroup.appendChild(deleteBtn);
layerItem.appendChild(btnGroup);
layerItem.addEventListener('click', function(e) {
if (e.target.closest('.layer-tree-btn')) return;
// レイヤーを選択状態にする
document.querySelectorAll('.layer-tree-item').forEach(item => {
item.classList.remove('selected');
});
this.classList.add('selected');
// レイヤーを中央に表示
if (layer.layer.getBounds) {
map.fitBounds(layer.layer.getBounds());
} else if (layer.layer.getLatLng) {
map.setView(layer.layer.getLatLng(), map.getZoom());
}
});
group.appendChild(layerItem);
});
layerTreeContent.appendChild(group);
}
});
// 初期状態で最初のグループを展開
const firstGroup = document.querySelector('.layer-tree-item-group');
if (firstGroup) {
firstGroup.classList.add('expanded');
const firstHeader = document.querySelector('.layer-tree-item');
if (firstHeader) {
firstHeader.querySelector('.layer-tree-toggle').textContent = '▾';
}
}
}
// レイヤーアイコンを取得
function getLayerIcon(type) {
const icons = {
'tile': '🧩',
'canvas': '🎨',
'svg': '🖌️',
'grid': '🔲',
'marker': '📍',
'polyline': '➖',
'polygon': '🔶',
'circle': '⭕',
'circlemarker': '🔵',
'layergroup': '📁',
'featuregroup': '📂',
'control': '🎚️',
'heatmap': '🔥',
'cluster': '👥',
'vectorgrid': '🧊'
};
return icons[type] || '🔘';
}
// レイヤー編集関数
function editLayer(layer) {
showLayerEditor();
// レイヤータイプに応じたタブを選択
let tabId = 'other';
if (['tile', 'canvas', 'svg', 'grid'].includes(layer.type)) {
tabId = 'base';
} else if (['marker', 'polyline', 'polygon', 'circle', 'circlemarker', 'geojson', 'image', 'video'].includes(layer.type)) {
tabId = 'overlay';
}
switchLayerTab(tabId);
// フォームに値を設定
document.getElementById(`${tabId}-layer-type`).value = layer.type;
if (tabId === 'base' && layer.type === 'tile') {
document.getElementById("tile-layer-url").value = layer.layer._url;
document.getElementById("tile-layer-attribution").value = layer.layer.options.attribution || '';
document.getElementById("tile-layer-options").value = JSON.stringify(
Object.fromEntries(
Object.entries(layer.layer.options)
.filter(([key]) => !['attribution'].includes(key))
), null, 2
);
} else if (tabId === 'overlay') {
document.getElementById("overlayer-options").value = JSON.stringify(layer.options, null, 2);
// 座標を設定
if (['polyline', 'polygon', 'circle', 'circlemarker', 'marker'].includes(layer.type)) {
overlayCoords = layer.layer.getLatLngs ? layer.layer.getLatLngs() :
layer.layer.getLatLng ? [layer.layer.getLatLng()] : [];
updateOverlayCoordsList();
}
} else if (tabId === 'other') {
document.getElementById("other-layer-options").value = JSON.stringify(layer.options, null, 2);
}
// 既存のレイヤーを削除
const index = layers.findIndex(l => l.id === layer.id);
if (index !== -1) {
layers.splice(index, 1);
map.removeLayer(layer.layer);
}
}
// レイヤー削除関数
function deleteLayer(layer) {
if (confirm(`レイヤー「${layer.name}」を削除しますか?`)) {
map.removeLayer(layer.layer);
layers = layers.filter(l => l.id !== layer.id);
saveCurrentMapToStorage();
updateLayerTree();
}
}
// プラグインマネージャーを表示
function showPluginManager() {
document.getElementById("plugin-manager").style.display = "block";
updatePluginList();
}
// プラグインマネージャーを非表示
function hidePluginManager() {
document.getElementById("plugin-manager").style.display = "none";
}
// プラグインを追加
function addPlugin() {
const pluginUrl = document.getElementById("plugin-url").value.trim();
if (!pluginUrl) {
alert("プラグインURLを入力してください");
return;
}
// 既に追加されているかチェック
if (plugins.some(p => p.url === pluginUrl)) {
alert("このプラグインは既に追加されています");
return;
}
// プラグインを追加
plugins.push({
id: 'plugin-' + Date.now(),
url: pluginUrl,
loaded: false
});
// プラグインを読み込み
loadPlugin(pluginUrl);
// プラグインリストを更新
updatePluginList();
// ストレージに保存
savePluginsToStorage();
document.getElementById("plugin-url").value = "";
}
// プラグインを読み込み
function loadPlugin(url) {
const script = document.createElement('script');
script.src = url;
script.onload = function() {
// プラグインの読み込み状態を更新
const plugin = plugins.find(p => p.url === url);
if (plugin) {
plugin.loaded = true;
updatePluginList();
savePluginsToStorage();
}
};
script.onerror = function() {
alert("プラグインの読み込みに失敗しました: " + url);
};
document.head.appendChild(script);
}
// プラグインリストを更新
function updatePluginList() {
const pluginList = document.getElementById("plugin-list");
pluginList.innerHTML = '';
if (plugins.length === 0) {
pluginList.innerHTML = '<div class="terminal-line">プラグインがありません</div>';
return;
}
plugins.forEach(plugin => {
const pluginItem = document.createElement('div');
pluginItem.className = 'plugin-item';
const pluginStatus = plugin.loaded ? '✅ 読み込み済み' : '⏳ 読み込み中...';
pluginItem.innerHTML = `
<div class="terminal-line">${plugin.url}</div>
<div class="terminal-line">${pluginStatus}</div>
<div class="plugin-item-actions">
<button class="hacker-btn secondary remove-plugin-btn" data-id="${plugin.id}">削除</button>
</div>
`;
pluginList.appendChild(pluginItem);
});
// 削除ボタンのイベントリスナーを追加
document.querySelectorAll('.remove-plugin-btn').forEach(btn => {
btn.addEventListener('click', function() {
const pluginId = this.dataset.id;
removePlugin(pluginId);
});
});
}
// プラグインを削除
function removePlugin(pluginId) {
if (confirm("このプラグインを削除しますか?ページをリロードするとプラグインの機能は利用できなくなります。")) {
plugins = plugins.filter(p => p.id !== pluginId);
updatePluginList();
savePluginsToStorage();
}
}
// プラグインをストレージから読み込み
function loadPluginsFromStorage() {
const savedPlugins = localStorage.getItem('mapEditorPlugins');
if (savedPlugins) {
plugins = JSON.parse(savedPlugins);
plugins.forEach(plugin => {
if (plugin.loaded) {
loadPlugin(plugin.url);
}
});
}
}
// プラグインをストレージに保存
function savePluginsToStorage() {
localStorage.setItem('mapEditorPlugins', JSON.stringify(plugins));
}
// マーカー編集モードのトグル
function toggleEditNextMarkerMode() {
if (nextMarkerEdit) {
// 編集モードをキャンセル
nextMarkerEdit = false;
document.getElementById("edit-next-marker").textContent = "次のマーカーを編集";
document.getElementById("edit-next-marker").classList.remove("danger");
document.getElementById("edit-next-marker").classList.add("secondary");
alert("編集モードをキャンセルしました。");
} else {
// 編集モードを開始
nextMarkerEdit = true;
document.getElementById("edit-next-marker").textContent = "編集をキャンセル";
document.getElementById("edit-next-marker").classList.remove("secondary");
document.getElementById("edit-next-marker").classList.add("danger");
alert("クリックして次のマーカーを編集します。");
}
}
// マーカーが存在するかどうかでボタンの状態を更新
function updateEditNextMarkerButton() {
const hasMarkers = mapHasMarkers();
const editBtn = document.getElementById("edit-next-marker");
if (hasMarkers) {
editBtn.classList.remove("disabled");
} else {
editBtn.classList.add("disabled");
// マーカーがない場合、編集モードをキャンセル
if (nextMarkerEdit) {
nextMarkerEdit = false;
editBtn.textContent = "次のマーカーを編集";
editBtn.classList.remove("danger");
editBtn.classList.add("secondary");
}
}
}
// マップにマーカーが存在するかチェック
function mapHasMarkers() {
let hasMarkers = false;
map.eachLayer((layer) => {
if (layer instanceof L.Marker) {
hasMarkers = true;
}
});
return hasMarkers;
}
// マーカーエディタを開く
function openEditor(marker) {
const latlng = marker.getLatLng();
document.getElementById("marker-lat").value = latlng.lat;
document.getElementById("marker-lng").value = latlng.lng;
document.getElementById("marker-popup").value = marker.getPopup() ? marker.getPopup().getContent() : "";
document.getElementById("marker-tooltip").value = marker.getTooltip() ? marker.getTooltip().getContent() : "";
document.getElementById("marker-editor").style.display = "block";
editingMarker = marker;
const icon = marker.options.icon;
if (icon && icon.options) {
if (icon.options.iconUrl) {
document.getElementById("marker-icon-url").value = icon.options.iconUrl;
document.getElementById("icon-preview").src = icon.options.iconUrl;
document.getElementById("icon-preview").style.display = 'block';
document.getElementById("icon-settings").style.display = "block";
}
document.getElementById("icon-width").value = icon.options.iconSize[0];
document.getElementById("icon-height").value = icon.options.iconSize[1];
document.getElementById("icon-width-value").textContent = icon.options.iconSize[0];
document.getElementById("icon-height-value").textContent = icon.options.iconSize[1];
}
updatePreviewSize();
}
// マーカーを保存
function saveMarker() {
if (editingMarker) {
const lat = parseFloat(document.getElementById("marker-lat").value);
const lng = parseFloat(document.getElementById("marker-lng").value);
const popupContent = document.getElementById("marker-popup").value;
const tooltipContent = document.getElementById("marker-tooltip").value;
const iconUrl = document.getElementById("icon-preview").src;
applyIconAndSaveMarker(lat, lng, popupContent, tooltipContent, iconUrl);
saveCurrentMapToStorage();
}
}
// アイコンを適用してマーカーを保存
function applyIconAndSaveMarker(lat, lng, popupContent, tooltipContent, iconUrl) {
const iconWidth = parseInt(document.getElementById("icon-width").value);
const iconHeight = parseInt(document.getElementById("icon-height").value);
editingMarker.setLatLng([lat, lng]);
if (iconUrl) {
const icon = L.icon({
iconUrl: iconUrl,
iconSize: [iconWidth, iconHeight],
iconAnchor: [iconWidth / 2, iconHeight],
popupAnchor: [0, -iconHeight],
tooltipAnchor: [iconWidth / 2, -iconHeight / 2]
});
editingMarker.setIcon(icon);
}
editingMarker.bindPopup(popupContent);
editingMarker.bindTooltip(tooltipContent);
document.getElementById("marker-editor").style.display = "none";
editingMarker = null;
}
// マーカーを削除
function deleteMarker() {
if (confirm("削除していいですか?")) {
map.removeLayer(editingMarker);
document.getElementById("marker-editor").style.display = "none";
editingMarker = null;
saveCurrentMapToStorage();
updateEditNextMarkerButton();
}
}
// アイコンをアップロード
function handleIconUpload() {
const file = this.files[0];
const preview = document.getElementById("icon-preview");
if (file) {
resizeImage(file, parseInt(document.getElementById("icon-width").value), parseInt(document.getElementById("icon-height").value), function(imageDataUrl) {
preview.src = imageDataUrl;
preview.style.display = "block";
document.getElementById("icon-settings").style.display = "block";
updatePreviewSize();
});
} else {
preview.style.display = "none";
document.getElementById("icon-settings").style.display = "none";
}
}
// URLからアイコンを読み込み
function loadIconFromUrl() {
const url = document.getElementById("marker-icon-url").value;
const preview = document.getElementById("icon-preview");
if (url) {
preview.src = url;
preview.onload = function() {
preview.style.display = "block";
document.getElementById("icon-settings").style.display = "block";
updatePreviewSize();
};
preview.onerror = function() {
alert("IMAGE LOAD FAILED. CHECK URL.");
preview.style.display = "none";
document.getElementById("icon-settings").style.display = "none";
};
} else {
preview.style.display = "none";
document.getElementById("icon-settings").style.display = "none";
}
}
// 画像をリサイズ
function resizeImage(file, width, height, callback) {
const reader = new FileReader();
reader.onload = function(e) {
const img = new Image();
img.onload = function() {
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
canvas.getContext("2d").drawImage(img, 0, 0, width, height);
callback(canvas.toDataURL());
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
// 幅の同期
function syncWidth(event) {
let value = event.target.value;
document.getElementById("icon-width").value = value;
document.getElementById("icon-width-input").value = value;
updatePreviewSize();
}
// 高さの同期
function syncHeight(event) {
let value = event.target.value;
document.getElementById("icon-height").value = value;
document.getElementById("icon-height-input").value = value;
updatePreviewSize();
}
// プレビューサイズを更新
function updatePreviewSize() {
var width = document.getElementById("icon-width").value;
var height = document.getElementById("icon-height").value;
var preview = document.getElementById("icon-preview");
preview.style.width = width + "px";
preview.style.height = height + "px";
document.getElementById("icon-width-value").textContent = width;
document.getElementById("icon-height-value").textContent = height;
if (editingMarker) {
var iconUrl = preview.src;
var icon = L.icon({
iconUrl: iconUrl,
iconSize: [width, height],
iconAnchor: [width / 2, height],
popupAnchor: [0, -height],
tooltipAnchor: [width / 2, -height / 2]
});
editingMarker.setIcon(icon);
saveCurrentMapToStorage();
}
}
// マーカーエディタのドラッグ移動を設定
function setupEditorDrag() {
const editor = document.getElementById("marker-editor");
let isDragging = false;
let offsetX, offsetY;
editor.addEventListener("mousedown", function(e) {
if (!e.target.closest("input, textarea, button")) {
isDragging = true;
offsetX = e.clientX - editor.getBoundingClientRect().left;
offsetY = e.clientY - editor.getBoundingClientRect().top;
}
});
document.addEventListener("mousemove", function(e) {
if (isDragging) {
editor.style.left = (e.clientX - offsetX) + "px";
editor.style.top = (e.clientY - offsetY) + "px";
}
});
document.addEventListener("mouseup", function() {
isDragging = false;
});
}
// 現在のマップを保存
function saveCurrentMapToStorage() {
const mapData = {
center: map.getCenter(),
zoom: map.getZoom(),
markers: [],
layers: [],
plugins: plugins
};
// マーカーを収集
map.eachLayer((layer) => {
if (layer instanceof L.Marker) {
const marker = layer;
const icon = marker.options.icon;
const { lat, lng } = marker.getLatLng();
mapData.markers.push({
lat: lat,
lng: lng,
iconUrl: icon.options.iconUrl,
iconSize: icon.options.iconSize,
popupContent: marker.getPopup() ? marker.getPopup().getContent() : '',
tooltipContent: marker.getTooltip() ? marker.getTooltip().getContent() : '',
});
}
});
// レイヤーを収集
layers.forEach(layer => {
const layerData = {
id: layer.id,
name: layer.name,
type: layer.type,
options: layer.options
};
// タイプに応じて追加データを保存
if (layer.type === 'polyline' || layer.type === 'polygon' ||
layer.type === 'circle' || layer.type === 'circlemarker' ||
layer.type === 'marker') {
if (layer.layer.getLatLngs) {
layerData.coords = layer.layer.getLatLngs();
} else if (layer.layer.getLatLng) {
layerData.coords = [layer.layer.getLatLng()];
}
} else if (layer.type === 'tile') {
layerData.url = layer.layer._url;
layerData.attribution = layer.layer.options.attribution;
}
mapData.layers.push(layerData);
});
if (currentMapName) {
// 既存のマップを更新
const savedMaps = JSON.parse(localStorage.getItem('savedMaps')) || {};
savedMaps[currentMapName] = mapData;
localStorage.setItem('savedMaps', JSON.stringify(savedMaps));
}
}
// マップ保存モーダルを表示
function showSaveMapModal() {
document.getElementById("save-map-modal").style.display = "block";
}
// マップ保存モーダルを非表示
function hideSaveMapModal() {
document.getElementById("save-map-modal").style.display = "none";
}
// マップ名を付けて保存
function saveCurrentMapWithName() {
const mapName = document.getElementById("save-map-name").value.trim();
if (!mapName) {
alert("マップ名を入力してください");
return;
}
const mapData = {
center: map.getCenter(),
zoom: map.getZoom(),
markers: [],
layers: [],
plugins: plugins
};
// マーカーを収集
map.eachLayer((layer) => {
if (layer instanceof L.Marker) {
const marker = layer;
const icon = marker.options.icon;
const { lat, lng } = marker.getLatLng();
mapData.markers.push({
lat: lat,
lng: lng,
iconUrl: icon.options.iconUrl,
iconSize: icon.options.iconSize,
popupContent: marker.getPopup() ? marker.getPopup().getContent() : '',
tooltipContent: marker.getTooltip() ? marker.getTooltip().getContent() : '',
});
}
});
// レイヤーを収集
layers.forEach(layer => {
const layerData = {
id: layer.id,
name: layer.name,
type: layer.type,
options: layer.options
};
// タイプに応じて追加データを保存
if (layer.type === 'polyline' || layer.type === 'polygon' ||
layer.type === 'circle' || layer.type === 'circlemarker' ||
layer.type === 'marker') {
if (layer.layer.getLatLngs) {
layerData.coords = layer.layer.getLatLngs();
} else if (layer.layer.getLatLng) {
layerData.coords = [layer.layer.getLatLng()];
}
} else if (layer.type === 'tile') {
layerData.url = layer.layer._url;
layerData.attribution = layer.layer.options.attribution;
}
mapData.layers.push(layerData);
});
const savedMaps = JSON.parse(localStorage.getItem('savedMaps')) || {};
savedMaps[mapName] = mapData;
localStorage.setItem('savedMaps', JSON.stringify(savedMaps));
currentMapName = mapName;
document.getElementById("save-map-name").value = "";
hideSaveMapModal();
alert(`マップ「${mapName}」を保存しました`);
}
// マップギャラリーを表示
function showGallery() {
const gallery = document.getElementById("gallery-container");
const mapList = document.getElementById("gallery-map-list");
mapList.innerHTML = "";
const savedMaps = JSON.parse(localStorage.getItem('savedMaps')) || {};
if (Object.keys(savedMaps).length === 0) {
mapList.innerHTML = '<div class="text-center py-4">保存されたマップはありません</div>';
} else {
for (const [name, data] of Object.entries(savedMaps)) {
const mapItem = document.createElement("div");
mapItem.className = "gallery-map-item";
// マップ名を編集可能にする
const mapNameElement = document.createElement("div");
mapNameElement.className = "gallery-map-title";
mapNameElement.textContent = name;
mapNameElement.dataset.name = name;
// マップ名の編集イベント
mapNameElement.addEventListener('click', function(e) {
if (e.target === this) {
editMapName(this);
}
});
mapItem.appendChild(mapNameElement);
// プレビュー情報
const previewDiv = document.createElement("div");
previewDiv.className = "gallery-map-preview";
let previewText = `マーカー数: ${data.markers.length}\n`;
previewText += `レイヤー数: ${data.layers.length}\n`;
previewText += `中心座標: ${data.center.lat.toFixed(4)}, ${data.center.lng.toFixed(4)}\n`;
previewText += `ズームレベル: ${data.zoom}`;
previewDiv.textContent = previewText;
mapItem.appendChild(previewDiv);
// アクションボタン
const actionsDiv = document.createElement("div");
actionsDiv.className = "gallery-map-actions";
const loadBtn = document.createElement("button");
loadBtn.className = "hacker-btn gallery-btn load-map-btn";
loadBtn.dataset.name = name;
loadBtn.textContent = "読み込み";
actionsDiv.appendChild(loadBtn);
const deleteBtn = document.createElement("button");
deleteBtn.className = "hacker-btn gallery-btn danger delete-map-btn";
deleteBtn.dataset.name = name;
deleteBtn.textContent = "削除";
actionsDiv.appendChild(deleteBtn);
mapItem.appendChild(actionsDiv);
mapList.appendChild(mapItem);
}
// イベントリスナーを追加
document.querySelectorAll('.load-map-btn').forEach(btn => {
btn.addEventListener('click', function() {
loadMapFromGallery(this.dataset.name);
});
});
document.querySelectorAll('.delete-map-btn').forEach(btn => {
btn.addEventListener('click', function() {
if (confirm(`マップ「${this.dataset.name}」を削除しますか?`)) {
deleteMapFromGallery(this.dataset.name);
}
});
});
}
gallery.style.display = "block";
}
// マップ名を編集
function editMapName(element) {
const oldName = element.dataset.name;
const savedMaps = JSON.parse(localStorage.getItem('savedMaps')) || {};
// 編集モードに入る
element.contentEditable = true;
element.classList.add('editing');
element.focus();
// 選択範囲を最後に移動
const range = document.createRange();
range.selectNodeContents(element);
range.collapse(false);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
// 編集終了時の処理
const handleBlur = function() {
element.contentEditable = false;
element.classList.remove('editing');
const newName = element.textContent.trim();
if (newName && newName !== oldName) {
if (savedMaps[newName]) {
alert("この名前のマップは既に存在します");
element.textContent = oldName;
return;
}
// マップ名を変更
savedMaps[newName] = savedMaps[oldName];
delete savedMaps[oldName];
localStorage.setItem('savedMaps', JSON.stringify(savedMaps));
// 現在のマップ名を更新
if (currentMapName === oldName) {
currentMapName = newName;
}
alert(`マップ名を「${oldName}」から「${newName}」に変更しました`);
} else {
element.textContent = oldName;
}
element.removeEventListener('blur', handleBlur);
element.removeEventListener('keydown', handleKeyDown);
};
// Enterキーで編集終了
const handleKeyDown = function(e) {
if (e.key === 'Enter') {
e.preventDefault();
element.blur();
}
};
element.addEventListener('blur', handleBlur);
element.addEventListener('keydown', handleKeyDown);
}
// マップギャラリーを非表示
function hideGallery() {
document.getElementById("gallery-container").style.display = "none";
}
// ギャラリーからマップを読み込み
function loadMapFromGallery(mapName) {
const savedMaps = JSON.parse(localStorage.getItem('savedMaps')) || {};
const mapData = savedMaps[mapName];
if (!mapData) {
alert("マップデータが見つかりません");
return;
}
// 現在のマップをクリア
clearCurrentMap();
// 新しいマップを読み込み
map.setView(mapData.center, mapData.zoom);
currentMapName = mapName; // 現在のマップ名を更新
// マーカーを追加
mapData.markers.forEach((markerData) => {
const icon = L.icon({
iconUrl: markerData.iconUrl,
iconSize: markerData.iconSize,
iconAnchor: [markerData.iconSize[0] / 2, markerData.iconSize[1]],
popupAnchor: [0, -markerData.iconSize[1]],
tooltipAnchor: [markerData.iconSize[0] / 2, -markerData.iconSize[1] / 2],
});
const marker = L.marker([markerData.lat, markerData.lng], { icon: icon }).addTo(map);
if (markerData.popupContent) marker.bindPopup(markerData.popupContent);
if (markerData.tooltipContent) marker.bindTooltip(markerData.tooltipContent);
marker.on("mouseover", function() {
hoveredMarker = marker;
});
marker.on("mouseout", function() {
if (hoveredMarker === marker) {
hoveredMarker = null;
}
});
});
// レイヤーを追加
layers = []; // レイヤーリストをリセット
mapData.layers.forEach(layerData => {
let layer;
switch (layerData.type) {
case 'tile':
layer = L.tileLayer(layerData.url, {
attribution: layerData.attribution,
...layerData.options
}).addTo(map);
break;
case 'canvas':
layer = L.canvas(layerData.options).addTo(map);
break;
case 'svg':
layer = L.svg(layerData.options).addTo(map);
break;
case 'grid':
layer = L.gridLayer(layerData.options).addTo(map);
break;
case 'polyline':
layer = L.polyline(layerData.coords, layerData.options).addTo(map);
break;
case 'polygon':
layer = L.polygon(layerData.coords, layerData.options).addTo(map);
break;
case 'circle':
layer = L.circle(layerData.coords[0], layerData.options).addTo(map);
break;
case 'circlemarker':
layer = L.circleMarker(layerData.coords[0], layerData.options).addTo(map);
break;
case 'marker':
layer = L.marker(layerData.coords[0], layerData.options).addTo(map);
break;
case 'layergroup':
layer = L.layerGroup().addTo(map);
break;
case 'featuregroup':
layer = L.featureGroup().addTo(map);
break;
case 'control':
// ベースレイヤーとオーバーレイレイヤーを収集
const baseLayers = {};
const overlays = {};
layers.forEach(l => {
if (l.type === 'tile' || l.type === 'canvas' || l.type === 'svg' || l.type === 'grid') {
baseLayers[l.name] = l.layer;
} else {
overlays[l.name] = l.layer;
}
});
layer = L.control.layers(baseLayers, overlays, layerData.options).addTo(map);
layerControls[layer._leaflet_id] = layer;
break;
}
if (layer) {
layers.push({
id: layerData.id,
name: layerData.name,
type: layerData.type,
layer: layer,
options: layerData.options
});
}
});
// プラグインを読み込み
plugins = mapData.plugins || [];
savePluginsToStorage();
// 未読み込みのプラグインを読み込む
plugins.forEach(plugin => {
if (!plugin.loaded) {
loadPlugin(plugin.url);
}
});
// 保存フォームにマップ名をセット
document.getElementById("save-map-name").value = currentMapName;
// ユーザーに通知
alert(`マップ「${mapName}」を読み込みました`);
hideGallery();
updateEditNextMarkerButton();
updateLayerTree();
}
// ギャラリーからマップを削除
function deleteMapFromGallery(mapName) {
const savedMaps = JSON.parse(localStorage.getItem('savedMaps')) || {};
delete savedMaps[mapName];
localStorage.setItem('savedMaps', JSON.stringify(savedMaps));
if (currentMapName === mapName) {
currentMapName = '';
}
showGallery(); // ギャラリーを更新
}
// 現在のマップをクリア
function clearCurrentMap() {
map.eachLayer(layer => {
if (!(layer instanceof L.TileLayer)) {
map.removeLayer(layer);
}
});
// レイヤーコントロールを削除
Object.values(layerControls).forEach(control => {
map.removeControl(control);
});
layerControls = {};
layers = [];
currentMapName = '';
updateEditNextMarkerButton();
updateLayerTree();
}
// HTMLを生成
function generateMapHTML() {
const markers = [];
map.eachLayer((layer) => {
if (layer instanceof L.Marker) {
const marker = layer;
const icon = marker.options.icon;
const iconUrl = icon.options.iconUrl;
const iconSize = icon.options.iconSize;
const latlng = marker.getLatLng();
const popupContent = marker.getPopup() ? marker.getPopup().getContent() : "";
const tooltipContent = marker.getTooltip() ? marker.getTooltip().getContent() : "";
markers.push({
lat: latlng.lat,
lng: latlng.lng,
iconUrl: iconUrl,
iconWidth: iconSize[0],
iconHeight: iconSize[1],
popupContent: popupContent,
tooltipContent: tooltipContent
});
}
});
const center = map.getCenter();
const zoom = map.getZoom();
// プラグインスクリプトを収集
let pluginScripts = '';
plugins.forEach(plugin => {
pluginScripts += `<script src="${plugin.url}"><\/script>\n`;
});
// レイヤーを収集
let layerScripts = '';
let layerControls = '';
let baseLayers = {};
let overlayLayers = {};
layers.forEach(layer => {
let layerScript = '';
let layerVarName = `layer_${layer.id.replace(/-/g, '_')}`;
switch (layer.type) {
case 'tile':
layerScript = `var ${layerVarName} = L.tileLayer('${layer.layer._url}', ${JSON.stringify(layer.layer.options)});\n`;
baseLayers[`"${layer.name}"`] = layerVarName;
break;
case 'polyline':
layerScript = `var ${layerVarName} = L.polyline(${JSON.stringify(layer.layer.getLatLngs())}, ${JSON.stringify(layer.layer.options)});\n`;
overlayLayers[`"${layer.name}"`] = layerVarName;
break;
case 'polygon':
layerScript = `var ${layerVarName} = L.polygon(${JSON.stringify(layer.layer.getLatLngs())}, ${JSON.stringify(layer.layer.options)});\n`;
overlayLayers[`"${layer.name}"`] = layerVarName;
break;
case 'circle':
layerScript = `var ${layerVarName} = L.circle([${layer.layer.getLatLng().lat}, ${layer.layer.getLatLng().lng}], ${JSON.stringify(layer.layer.options)});\n`;
overlayLayers[`"${layer.name}"`] = layerVarName;
break;
case 'circlemarker':
layerScript = `var ${layerVarName} = L.circleMarker([${layer.layer.getLatLng().lat}, ${layer.layer.getLatLng().lng}], ${JSON.stringify(layer.layer.options)});\n`;
overlayLayers[`"${layer.name}"`] = layerVarName;
break;
case 'marker':
layerScript = `var ${layerVarName} = L.marker([${layer.layer.getLatLng().lat}, ${layer.layer.getLatLng().lng}], ${JSON.stringify(layer.layer.options)});\n`;
overlayLayers[`"${layer.name}"`] = layerVarName;
break;
case 'layergroup':
layerScript = `var ${layerVarName} = L.layerGroup();\n`;
overlayLayers[`"${layer.name}"`] = layerVarName;
break;
case 'featuregroup':
layerScript = `var ${layerVarName} = L.featureGroup();\n`;
overlayLayers[`"${layer.name}"`] = layerVarName;
break;
case 'control':
// コントロールは別途処理
break;
}
layerScripts += layerScript;
});
// レイヤーコントロールを生成
if (Object.keys(baseLayers).length > 0 || Object.keys(overlayLayers).length > 0) {
layerControls = `L.control.layers({\n ${Object.keys(baseLayers).join(',\n ')}\n}, {\n ${Object.keys(overlayLayers).join(',\n ')}\n}).addTo(map);\n`;
}
// レイヤーをマップに追加
let addLayersScript = '';
Object.values(baseLayers).forEach(layerVar => {
addLayersScript += `${layerVar}.addTo(map);\n`;
});
Object.values(overlayLayers).forEach(layerVar => {
addLayersScript += `${layerVar}.addTo(map);\n`;
});
let html = `<div id="map" style="height: 600px; width: 100%;">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css" />
${pluginScripts}
<script src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"><\/script>
<script>
var map = L.map('map').setView([${center.lat}, ${center.lng}], ${zoom});
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap contributors'
}).addTo(map);
${layerScripts}
${addLayersScript}
${layerControls}
${markers.map(marker => `
var icon = L.icon({
iconUrl: '${marker.iconUrl}',
iconSize: [${marker.iconWidth}, ${marker.iconHeight}],
iconAnchor: [${marker.iconWidth} / 2, ${marker.iconHeight}],
popupAnchor: [0, -${marker.iconHeight}],
tooltipAnchor: [${marker.iconWidth} / 2, -${marker.iconHeight} / 2]
});
var marker = L.marker([${marker.lat}, ${marker.lng}], {
icon: icon,
zIndexOffset: 1000
}).addTo(map);
${marker.popupContent ? `marker.bindPopup('${marker.popupContent.replace(/'/g, "\\'").replace(/<\/script>/g, "<\\/script>")}');` : ""}
${marker.tooltipContent ? `marker.bindTooltip('${marker.tooltipContent.replace(/'/g, "\\'").replace(/<\/script>/g, "<\\/script>")}');` : ""}
`).join("\n")}
<\/script>
`;
html = html.replace(/iconUrl: 'marker-icon\.png'/g, `iconUrl: '${location.origin}/marker-icon.png'`);
document.getElementById("output-html").value = html;
const input = document.getElementById('output-html').value;
const output = document.getElementById('output-html');
output.innerHTML = hljs.highlight('html', input).value;
output.classList.add('hljs');
}
// HTMLをクリップボードにコピー
function copyHTMLToClipboard() {
const textToCopy = document.getElementById("output-html").innerText;
navigator.clipboard.writeText(textToCopy).then(() => {
alert("コピーしました。");
}).catch(err => {
console.error('コピーに失敗しました:', err);
});
}
</script>
</body>
</html>