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