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