|
<!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> |