Files
2026-06-18 11:46:16 +02:00

2714 lines
64 KiB
HTML

<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Editable Star Map</title>
<style>
:root {
--panel: #111827;
--panel-2: #0b1220;
--line: #d6b21e;
--line-alt: #3487d9;
--line-muted: #7f8fa6;
--text: #eef6ff;
--muted: #9ba9bd;
--accent: #69b7ff;
--danger: #ef4444;
--ok: #30c48d;
--border: rgba(255, 255, 255, 0.16);
font-family: Inter, Segoe UI, Roboto, Arial, sans-serif;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background: #05080f;
color: var(--text);
overflow: hidden;
}
button,
input,
textarea,
select {
font: inherit;
}
.app {
display: grid;
grid-template-columns: minmax(0, 1fr) 320px;
grid-template-rows: auto minmax(0, 1fr);
height: 100vh;
}
.toolbar {
grid-column: 1 / -1;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: linear-gradient(180deg, #121a2a, #0d1422);
border-bottom: 1px solid var(--border);
min-width: 0;
}
.brand {
font-weight: 800;
margin-right: 8px;
white-space: nowrap;
}
.tool-group {
display: flex;
align-items: center;
gap: 6px;
padding-right: 10px;
border-right: 1px solid var(--border);
min-width: 0;
}
.tool-button,
.action-button {
border: 1px solid var(--border);
color: var(--text);
background: #172033;
padding: 8px 10px;
border-radius: 6px;
cursor: pointer;
min-height: 36px;
white-space: nowrap;
}
.tool-button:hover,
.action-button:hover {
border-color: rgba(105, 183, 255, 0.65);
}
.tool-button.is-active {
background: #1d4f7a;
border-color: #72c6ff;
box-shadow: 0 0 0 2px rgba(105, 183, 255, 0.18) inset;
}
.danger {
background: #35151c;
color: #ffe8ec;
border-color: rgba(239, 68, 68, 0.45);
}
.background-label {
display: flex;
align-items: center;
gap: 7px;
color: var(--muted);
white-space: nowrap;
}
input[type="color"] {
width: 36px;
height: 34px;
padding: 2px;
border: 1px solid var(--border);
border-radius: 6px;
background: #172033;
}
.map-wrap {
position: relative;
min-width: 0;
min-height: 0;
background: #05080f;
overflow: hidden;
}
.map-wrap::before {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(circle at 19% 42%, rgba(38, 126, 255, 0.26), transparent 23%),
radial-gradient(circle at 52% 76%, rgba(241, 147, 28, 0.2), transparent 22%),
radial-gradient(circle at 88% 27%, rgba(23, 190, 165, 0.22), transparent 24%),
radial-gradient(circle at 7% 82%, rgba(151, 46, 122, 0.16), transparent 19%);
pointer-events: none;
}
.map {
position: relative;
width: 100%;
height: 100%;
display: block;
touch-action: none;
background-color: var(--map-bg, #05080f);
}
.sector-border {
fill: none;
stroke: rgba(145, 170, 204, 0.42);
stroke-width: 2;
stroke-dasharray: 8 4;
pointer-events: none;
}
.grid-line {
stroke: rgba(145, 170, 204, 0.16);
stroke-width: 1;
pointer-events: none;
}
.grid-label {
fill: rgba(220, 235, 255, 0.45);
font-size: 12px;
font-weight: 700;
text-anchor: middle;
pointer-events: none;
user-select: none;
}
.connection {
stroke: var(--line);
stroke-width: 1.5;
opacity: 0.7;
cursor: pointer;
pointer-events: none;
}
.connection.trade {
stroke: var(--line-alt);
opacity: 0.85;
}
.connection.neutral {
stroke: var(--line-muted);
opacity: 0.75;
}
.connection-hit {
stroke: transparent;
stroke-width: 28;
cursor: pointer;
}
.connection.is-selected {
stroke: #ffffff;
stroke-width: 3;
opacity: 1;
}
.system {
cursor: grab;
}
.system:active {
cursor: grabbing;
}
.system-hit {
fill: transparent;
stroke: transparent;
pointer-events: all;
}
.system-core {
fill: #d9f0ff;
stroke: #81c8ff;
stroke-width: 2;
filter: drop-shadow(0 0 7px #62b9ff) drop-shadow(0 0 16px rgba(100, 179, 255, 0.8));
}
.system.is-selected .system-core {
stroke: #ffffff;
stroke-width: 4;
filter: drop-shadow(0 0 9px #ffffff) drop-shadow(0 0 20px #55baff);
}
.system.is-pending .system-core {
stroke: #ffd65a;
stroke-width: 4;
}
.system-label {
fill: #ffffff;
stroke: rgba(0, 0, 0, 0.88);
stroke-width: 4;
paint-order: stroke;
font-size: 19px;
font-weight: 800;
text-anchor: middle;
pointer-events: auto;
user-select: none;
cursor: default;
}
.sidebar {
display: flex;
flex-direction: column;
gap: 14px;
min-width: 0;
min-height: 0;
padding: 14px;
background: var(--panel);
border-left: 1px solid var(--border);
overflow: auto;
}
.section {
border: 1px solid var(--border);
border-radius: 8px;
background: var(--panel-2);
padding: 12px;
}
.section h2 {
margin: 0 0 10px;
font-size: 16px;
}
.field {
display: grid;
gap: 6px;
margin-top: 10px;
}
.field label {
color: var(--muted);
font-size: 13px;
}
.text-input,
textarea,
select {
width: 100%;
border: 1px solid var(--border);
border-radius: 6px;
background: #09101d;
color: var(--text);
padding: 9px 10px;
outline: none;
}
textarea {
min-height: 140px;
resize: vertical;
line-height: 1.4;
}
.hint,
.selection-meta {
color: var(--muted);
font-size: 13px;
line-height: 1.45;
}
.selection-meta {
margin: 0;
}
.hidden {
display: none;
}
.action-row {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 12px;
}
.status {
margin-left: auto;
color: var(--muted);
font-size: 13px;
min-width: 160px;
text-align: right;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mobile-note {
display: none;
}
@media (max-width: 880px) {
body {
overflow: auto;
}
.app {
grid-template-columns: 1fr;
grid-template-rows: auto 68vh auto;
min-height: 100vh;
height: auto;
}
.toolbar {
flex-wrap: wrap;
}
.sidebar {
border-left: 0;
border-top: 1px solid var(--border);
}
.status {
text-align: left;
margin-left: 0;
flex-basis: 100%;
}
.mobile-note {
display: block;
}
}
</style>
</head>
<body>
<div class="app">
<header class="toolbar">
<div class="brand">Star Map Editor</div>
<div class="tool-group" role="group" aria-label="Werkzeuge">
<button class="tool-button is-active" data-mode="select" type="button" title="Auswaehlen und verschieben">Auswahl</button>
<button class="tool-button" data-mode="add" type="button" title="In die Karte klicken, um ein System anzulegen">Punkt</button>
<button class="tool-button" data-mode="connect" type="button" title="Zwei Systeme anklicken, um sie zu verbinden">Verbindung</button>
</div>
<div class="tool-group">
<button class="action-button danger" id="deleteSelected" type="button">Loeschen</button>
<button class="action-button" id="resetMap" type="button">Vorlage laden</button>
</div>
<div class="tool-group" role="group" aria-label="Zoom">
<button class="action-button" id="zoomOut" type="button" title="Rauszoomen">-</button>
<button class="action-button" id="resetZoom" type="button" title="Zoom zuruecksetzen">1:1</button>
<button class="action-button" id="zoomIn" type="button" title="Ranzoomen">+</button>
</div>
<div class="tool-group">
<label class="background-label" for="backgroundColor">Hintergrund <input id="backgroundColor" type="color" value="#05080f"></label>
<span class="background-label">Raster aktiv</span>
</div>
<div class="status" id="status">Bereit</div>
</header>
<main class="map-wrap" id="mapWrap">
<svg class="map" id="map" viewBox="0 0 1296 1296" aria-label="Editierbare Sternenkarte">
<defs>
<radialGradient id="nodeGlow" cx="50%" cy="50%" r="55%">
<stop offset="0%" stop-color="#ffffff"></stop>
<stop offset="42%" stop-color="#9bd5ff"></stop>
<stop offset="100%" stop-color="#2b6dad"></stop>
</radialGradient>
</defs>
<g id="gridLayer"></g>
<rect x="55" y="59" width="984" height="984" class="sector-border"></rect>
<g id="connections"></g>
<g id="connectionHits"></g>
<g id="systems"></g>
</svg>
</main>
<aside class="sidebar">
<section class="section">
<h2>Auswahl</h2>
<p class="selection-meta" id="selectionMeta">Kein Objekt ausgewaehlt.</p>
<div id="systemEditor" class="hidden">
<div class="field">
<label for="systemName">Systemname</label>
<input class="text-input" id="systemName" type="text" autocomplete="off">
</div>
<div class="field">
<label for="systemColor">Farbe</label>
<input id="systemColor" type="color" value="#d9f0ff">
</div>
<div class="field">
<label for="systemNote">Notiz</label>
<textarea id="systemNote" placeholder="Notiz fuer dieses System"></textarea>
</div>
</div>
<div id="connectionEditor" class="hidden">
<div class="field">
<label for="connectionType">Linientyp</label>
<select id="connectionType">
<option value="standard">Gold</option>
<option value="trade">Blau</option>
<option value="neutral">Grau</option>
</select>
</div>
</div>
<div class="action-row">
<button class="action-button" id="centerSelection" type="button">Zentrieren</button>
</div>
</section>
<section class="section">
<h2>Bearbeitung</h2>
<p class="hint">Auswahl: System anklicken, Name/Notiz bearbeiten oder Punkt ziehen. Punkt: freie Stelle anklicken. Verbindung: zwei Systeme nacheinander anklicken. Neue und verschobene Systeme rasten immer auf die naechste Rasterzelle ein.</p>
<p class="hint mobile-note">Auf Touchscreens klappt Verschieben am besten im Auswahlmodus mit kurzem Halten und Ziehen.</p>
</section>
<section class="section">
<h2>Daten</h2>
<div class="action-row">
<button class="action-button" id="saveRemote" type="button">Speichern</button>
<button class="action-button" id="loadRemote" type="button">Laden</button>
<button class="action-button" id="exportJson" type="button">Export</button>
<button class="action-button" id="importJson" type="button">Import</button>
</div>
<div class="field">
<label for="dataBox">JSON</label>
<textarea id="dataBox" placeholder="Exportierte Karte oder JSON zum Importieren"></textarea>
</div>
</section>
</aside>
</div>
<script type="application/json" id="initial-map-data">
{
"source": "universe.ini",
"background": "#05080f",
"grid": {
"minCol": 0,
"minRow": 0,
"maxCol": 16,
"maxRow": 16,
"cellSize": 72,
"margin": 72
},
"viewBox": {
"x": 0,
"y": 0,
"width": 1296,
"height": 1296
},
"systems": [
{
"id": "li05",
"nickname": "Li05",
"name": "Alaska",
"file": "systems\\Li05\\Li05.ini",
"gridX": 9,
"gridY": 7,
"stridName": 196613,
"navMapScale": 1.5,
"visit": 0,
"note": "",
"x": 720,
"y": 576
},
{
"id": "iw01",
"nickname": "Iw01",
"name": "Bering",
"file": "systems\\Iw01\\Iw01.ini",
"gridX": 9,
"gridY": 9,
"stridName": 196643,
"navMapScale": 2,
"visit": 0,
"note": "",
"x": 720,
"y": 720
},
{
"id": "li02",
"nickname": "Li02",
"name": "California",
"file": "systems\\Li02\\Li02.ini",
"gridX": 6,
"gridY": 9,
"stridName": 196610,
"navMapScale": 1,
"visit": 0,
"note": "",
"x": 504,
"y": 720
},
{
"id": "br02",
"nickname": "Br02",
"name": "Cambridge",
"file": "systems\\Br02\\Br02.ini",
"gridX": 4,
"gridY": 10,
"stridName": 196615,
"navMapScale": 1,
"visit": 0,
"note": "",
"x": 360,
"y": 792
},
{
"id": "ku06",
"nickname": "Ku06",
"name": "Chugoku",
"file": "systems\\Ku06\\Ku06.ini",
"gridX": 7,
"gridY": 2,
"stridName": 196631,
"navMapScale": 2,
"visit": 0,
"note": "",
"x": 576,
"y": 216
},
{
"id": "li03",
"nickname": "Li03",
"name": "Colorado",
"file": "systems\\Li03\\Li03.ini",
"gridX": 7,
"gridY": 7,
"stridName": 196611,
"navMapScale": 1,
"visit": 0,
"note": "",
"x": 576,
"y": 576
},
{
"id": "iw04",
"nickname": "Iw04",
"name": "Cortez",
"file": "systems\\Iw04\\Iw04.ini",
"gridX": 5,
"gridY": 9,
"stridName": 196646,
"navMapScale": 2,
"visit": 0,
"note": "",
"x": 432,
"y": 720
},
{
"id": "rh05",
"nickname": "Rh05",
"name": "Dresden",
"file": "systems\\Rh05\\Rh05.ini",
"gridX": 11,
"gridY": 12,
"stridName": 196624,
"navMapScale": 2,
"visit": 0,
"note": "",
"x": 864,
"y": 936
},
{
"id": "br06",
"nickname": "Br06",
"name": "Dublin",
"file": "systems\\Br06\\Br06.ini",
"gridX": 0,
"gridY": 10,
"stridName": 196619,
"navMapScale": 2,
"visit": 0,
"note": "",
"x": 72,
"y": 792
},
{
"id": "br05",
"nickname": "Br05",
"name": "Edinburgh",
"file": "systems\\Br05\\Br05.ini",
"gridX": 1,
"gridY": 13,
"stridName": 196618,
"navMapScale": 2,
"visit": 0,
"note": "",
"x": 144,
"y": 1008
},
{
"id": "rh02",
"nickname": "Rh02",
"name": "Frankfurt",
"file": "systems\\Rh02\\Rh02.ini",
"gridX": 10,
"gridY": 10,
"stridName": 196621,
"navMapScale": 1,
"visit": 0,
"note": "",
"x": 792,
"y": 792
},
{
"id": "iw06",
"nickname": "Iw06",
"name": "Galileo",
"file": "systems\\Iw06\\Iw06.ini",
"gridX": 8,
"gridY": 6,
"stridName": 196648,
"navMapScale": 2,
"visit": 0,
"note": "",
"x": 648,
"y": 504
},
{
"id": "rh04",
"nickname": "Rh04",
"name": "Hamburg",
"file": "systems\\Rh04\\Rh04.ini",
"gridX": 12,
"gridY": 10,
"stridName": 196623,
"navMapScale": 1,
"visit": 0,
"note": "",
"x": 936,
"y": 792
},
{
"id": "ku05",
"nickname": "Ku05",
"name": "Hokkaido",
"file": "systems\\Ku05\\Ku05.ini",
"gridX": 7,
"gridY": 3,
"stridName": 196630,
"navMapScale": 2,
"visit": 0,
"note": "",
"x": 576,
"y": 288
},
{
"id": "ku04",
"nickname": "Ku04",
"name": "Honshu",
"file": "systems\\Ku04\\Ku04.ini",
"gridX": 8,
"gridY": 4,
"stridName": 196629,
"navMapScale": 1,
"visit": 0,
"note": "",
"x": 648,
"y": 360
},
{
"id": "iw02",
"nickname": "Iw02",
"name": "Hudson",
"file": "systems\\Iw02\\Iw02.ini",
"gridX": 9,
"gridY": 10,
"stridName": 196644,
"navMapScale": 2,
"visit": 0,
"note": "",
"x": 720,
"y": 792
},
{
"id": "iw05",
"nickname": "Iw05",
"name": "Kepler",
"file": "systems\\Iw05\\Iw05.ini",
"gridX": 6,
"gridY": 6,
"stridName": 196647,
"navMapScale": 2,
"visit": 0,
"note": "",
"x": 504,
"y": 504
},
{
"id": "ku03",
"nickname": "Ku03",
"name": "Kyushu",
"file": "systems\\Ku03\\Ku03.ini",
"gridX": 6,
"gridY": 4,
"stridName": 196628,
"navMapScale": 1,
"visit": 0,
"note": "",
"x": 504,
"y": 360
},
{
"id": "br04",
"nickname": "Br04",
"name": "Leeds",
"file": "systems\\Br04\\Br04.ini",
"gridX": 2,
"gridY": 9,
"stridName": 196617,
"navMapScale": 1,
"visit": 0,
"note": "",
"x": 216,
"y": 720
},
{
"id": "iw03",
"nickname": "Iw03",
"name": "Magellan",
"file": "systems\\Iw03\\Iw03.ini",
"gridX": 5,
"gridY": 10,
"stridName": 196645,
"navMapScale": 2,
"visit": 0,
"note": "",
"x": 432,
"y": 792
},
{
"id": "br03",
"nickname": "Br03",
"name": "Manchester",
"file": "systems\\Br03\\Br03.ini",
"gridX": 3,
"gridY": 12,
"stridName": 196616,
"navMapScale": 1,
"visit": 0,
"note": "",
"x": 288,
"y": 936
},
{
"id": "rh01",
"nickname": "Rh01",
"name": "New Berlin",
"file": "systems\\Rh01\\Rh01.ini",
"gridX": 11,
"gridY": 11,
"stridName": 196620,
"navMapScale": 1,
"visit": 0,
"note": "",
"x": 864,
"y": 864
},
{
"id": "br01",
"nickname": "Br01",
"name": "New London",
"file": "systems\\Br01\\Br01.ini",
"gridX": 2,
"gridY": 11,
"stridName": 196614,
"navMapScale": 1,
"visit": 0,
"note": "",
"x": 216,
"y": 864
},
{
"id": "li06",
"nickname": "LI06",
"name": "New Nevada",
"file": "systems\\LI06\\LI06.ini",
"gridX": 6,
"gridY": 8,
"stridName": 450000,
"navMapScale": 1.82,
"visit": 1,
"note": "",
"x": 504,
"y": 648
},
{
"id": "ku01",
"nickname": "Ku01",
"name": "New Tokyo",
"file": "systems\\Ku01\\Ku01.ini",
"gridX": 7,
"gridY": 4,
"stridName": 196626,
"navMapScale": 1,
"visit": 0,
"note": "",
"x": 576,
"y": 360
},
{
"id": "li01",
"nickname": "Li01",
"name": "New York",
"file": "Systems\\Li01\\Li01.ini",
"gridX": 7,
"gridY": 8,
"stridName": 196609,
"navMapScale": 1,
"visit": 1,
"note": "",
"x": 576,
"y": 648
},
{
"id": "bw04",
"nickname": "Bw04",
"name": "Omega-11",
"file": "systems\\Bw04\\Bw04.ini",
"gridX": 8,
"gridY": 13,
"stridName": 196636,
"navMapScale": 2.5,
"visit": 0,
"note": "",
"x": 648,
"y": 1008
},
{
"id": "bw01",
"nickname": "Bw01",
"name": "Omega-3",
"file": "systems\\Bw01\\Bw01.ini",
"gridX": 5,
"gridY": 12,
"stridName": 196633,
"navMapScale": 2,
"visit": 0,
"note": "",
"x": 432,
"y": 936
},
{
"id": "bw05",
"nickname": "Bw05",
"name": "Omega-41",
"file": "systems\\Bw05\\Bw05.ini",
"gridX": 11,
"gridY": 6,
"stridName": 196637,
"navMapScale": 2,
"visit": 0,
"note": "",
"x": 864,
"y": 504
},
{
"id": "bw02",
"nickname": "Bw02",
"name": "Omega-5",
"file": "systems\\Bw02\\Bw02.ini",
"gridX": 5,
"gridY": 14,
"stridName": 196634,
"navMapScale": 2,
"visit": 0,
"note": "",
"x": 432,
"y": 1080
},
{
"id": "bw03",
"nickname": "Bw03",
"name": "Omega-7",
"file": "systems\\Bw03\\Bw03.ini",
"gridX": 7,
"gridY": 12,
"stridName": 196635,
"navMapScale": 3,
"visit": 0,
"note": "",
"x": 576,
"y": 936
},
{
"id": "ew02",
"nickname": "Ew02",
"name": "Omicron Alpha",
"file": "systems\\Ew02\\Ew02.ini",
"gridX": 10,
"gridY": 1,
"stridName": 196650,
"navMapScale": 4,
"visit": 0,
"note": "",
"x": 792,
"y": 144
},
{
"id": "hi02",
"nickname": "Hi02",
"name": "Omicron Beta",
"file": "systems\\Hi02\\Hi02.ini",
"gridX": 13,
"gridY": 14,
"stridName": 196654,
"navMapScale": 2,
"visit": 0,
"note": "",
"x": 1008,
"y": 1080
},
{
"id": "st02",
"nickname": "St02",
"name": "Omicron Beta",
"file": "systems\\St02\\St02.ini",
"gridX": 13,
"gridY": 4,
"stridName": 196656,
"navMapScale": 2,
"visit": 128,
"note": "",
"x": 1008,
"y": 360
},
{
"id": "ew03",
"nickname": "Ew03",
"name": "Omicron Gamma",
"file": "systems\\Ew03\\Ew03.ini",
"gridX": 10,
"gridY": 14,
"stridName": 196651,
"navMapScale": 2,
"visit": 0,
"note": "",
"x": 792,
"y": 1080
},
{
"id": "st04",
"nickname": "ST04",
"name": "Omicron Lambda",
"file": "systems\\St04\\St04.ini",
"gridX": 15,
"gridY": 5,
"stridName": 198004,
"navMapScale": 2.125,
"visit": 0,
"note": "",
"x": 1152,
"y": 432
},
{
"id": "fp7_system",
"nickname": "FP7_system",
"name": "Omicron Major",
"file": "systems\\FP7\\FP7_system.ini",
"gridX": 16,
"gridY": 6,
"stridName": 196657,
"navMapScale": 3,
"visit": 128,
"note": "",
"x": 1224,
"y": 504
},
{
"id": "st03",
"nickname": "St03",
"name": "Omicron Major",
"file": "systems\\St03\\St03.ini",
"gridX": 14,
"gridY": 3,
"stridName": 196657,
"navMapScale": 2,
"visit": 128,
"note": "",
"x": 1080,
"y": 288
},
{
"id": "st03b",
"nickname": "St03b",
"name": "Omicron Major",
"file": "systems\\St03b\\St03b.ini",
"gridX": 15,
"gridY": 3,
"stridName": 196657,
"navMapScale": 2,
"visit": 128,
"note": "",
"x": 1152,
"y": 288
},
{
"id": "st01",
"nickname": "St01",
"name": "Omicron Minor",
"file": "systems\\St01\\St01.ini",
"gridX": 14,
"gridY": 5,
"stridName": 196655,
"navMapScale": 2,
"visit": 128,
"note": "",
"x": 1080,
"y": 432
},
{
"id": "st05",
"nickname": "St05",
"name": "Omicron Prime",
"file": "systems\\ST05\\st05.ini",
"gridX": 12,
"gridY": 16,
"stridName": 454900,
"navMapScale": 2,
"visit": 0,
"note": "",
"x": 936,
"y": 1224
},
{
"id": "ew04",
"nickname": "Ew04",
"name": "Omicron Theta",
"file": "systems\\Ew04\\Ew04.ini",
"gridX": 15,
"gridY": 12,
"stridName": 196652,
"navMapScale": 2.5,
"visit": 0,
"note": "",
"x": 1152,
"y": 936
},
{
"id": "li07",
"nickname": "LI07",
"name": "Puerto Rico",
"file": "systems\\LI07\\LI07.ini",
"gridX": 4,
"gridY": 7,
"stridName": 450100,
"navMapScale": 2,
"visit": 0,
"note": "",
"x": 360,
"y": 576
},
{
"id": "ku02",
"nickname": "Ku02",
"name": "Shikoku",
"file": "systems\\Ku02\\Ku02.ini",
"gridX": 7,
"gridY": 5,
"stridName": 196627,
"navMapScale": 1,
"visit": 0,
"note": "",
"x": 576,
"y": 432
},
{
"id": "bw11",
"nickname": "Bw11",
"name": "Sigma 2",
"file": "systems\\Bw11\\Bw11.ini",
"gridX": 10,
"gridY": 7,
"stridName": 196608,
"navMapScale": 2,
"visit": 0,
"note": "",
"x": 792,
"y": 576
},
{
"id": "bw06",
"nickname": "Bw06",
"name": "Sigma-13",
"file": "systems\\Bw06\\Bw06.ini",
"gridX": 12,
"gridY": 6,
"stridName": 196638,
"navMapScale": 2.5,
"visit": 0,
"note": "",
"x": 936,
"y": 504
},
{
"id": "bw07",
"nickname": "Bw07",
"name": "Sigma-17",
"file": "systems\\Bw07\\Bw07.ini",
"gridX": 11,
"gridY": 4,
"stridName": 196639,
"navMapScale": 2,
"visit": 0,
"note": "",
"x": 864,
"y": 360
},
{
"id": "bw08",
"nickname": "Bw08",
"name": "Sigma-19",
"file": "systems\\Bw08\\Bw08.ini",
"gridX": 2,
"gridY": 5,
"stridName": 196640,
"navMapScale": 3,
"visit": 0,
"note": "",
"x": 216,
"y": 432
},
{
"id": "rh03",
"nickname": "Rh03",
"name": "Stuttgart",
"file": "systems\\Rh03\\Rh03.ini",
"gridX": 10,
"gridY": 11,
"stridName": 196622,
"navMapScale": 1,
"visit": 0,
"note": "",
"x": 792,
"y": 864
},
{
"id": "bw10",
"nickname": "Bw10",
"name": "Tau-29",
"file": "systems\\Bw10\\Bw10.ini",
"gridX": 2,
"gridY": 7,
"stridName": 196642,
"navMapScale": 2,
"visit": 0,
"note": "",
"x": 216,
"y": 576
},
{
"id": "bw09",
"nickname": "Bw09",
"name": "Tau-31",
"file": "systems\\Bw09\\Bw09.ini",
"gridX": 3,
"gridY": 6,
"stridName": 196641,
"navMapScale": 2,
"visit": 0,
"note": "",
"x": 288,
"y": 504
},
{
"id": "ew01",
"nickname": "Ew01",
"name": "Tau-37",
"file": "systems\\Ew01\\Ew01.ini",
"gridX": 4,
"gridY": 1,
"stridName": 196649,
"navMapScale": 3.5,
"visit": 0,
"note": "",
"x": 360,
"y": 144
},
{
"id": "li04",
"nickname": "Li04",
"name": "Texas",
"file": "systems\\Li04\\Li04.ini",
"gridX": 8,
"gridY": 9,
"stridName": 196612,
"navMapScale": 1,
"visit": 0,
"note": "",
"x": 648,
"y": 720
},
{
"id": "ku07",
"nickname": "Ku07",
"name": "Tohoku",
"file": "systems\\Ku07\\Ku07.ini",
"gridX": 9,
"gridY": 2,
"stridName": 196632,
"navMapScale": 1.7,
"visit": 0,
"note": "",
"x": 720,
"y": 216
},
{
"id": "hi01",
"nickname": "Hi01",
"name": "Unknown Alpha",
"file": "systems\\Hi01\\Hi01.ini",
"gridX": 8,
"gridY": 0,
"stridName": 196653,
"navMapScale": 2,
"visit": 0,
"note": "",
"x": 648,
"y": 72
},
{
"id": "st02c",
"nickname": "St02c",
"name": "Unknown Beta",
"file": "systems\\St02c\\St02c.ini",
"gridX": 13,
"gridY": 1,
"stridName": 196657,
"navMapScale": 3,
"visit": 128,
"note": "",
"x": 1008,
"y": 144
},
{
"id": "ew06",
"nickname": "Ew06",
"name": "Unknown Delta",
"file": "systems\\Ew06\\Ew06.ini",
"gridX": 15,
"gridY": 9,
"stridName": 198011,
"navMapScale": 3,
"visit": 0,
"note": "",
"x": 1152,
"y": 720
},
{
"id": "ew05",
"nickname": "Ew05",
"name": "Unknown Gamma",
"file": "systems\\Ew05\\Ew05.ini",
"gridX": 15,
"gridY": 0,
"stridName": 198010,
"navMapScale": 3,
"visit": 0,
"note": "",
"x": 1152,
"y": 72
}
],
"connections": [
{
"id": "c-br01-br02",
"from": "br01",
"to": "br02",
"type": "standard"
},
{
"id": "c-br01-br03",
"from": "br01",
"to": "br03",
"type": "standard"
},
{
"id": "c-br01-br04",
"from": "br01",
"to": "br04",
"type": "standard"
},
{
"id": "c-br01-br05",
"from": "br01",
"to": "br05",
"type": "standard"
},
{
"id": "c-br02-br04",
"from": "br02",
"to": "br04",
"type": "neutral"
},
{
"id": "c-br02-iw03",
"from": "br02",
"to": "iw03",
"type": "standard"
},
{
"id": "c-br02-iw04",
"from": "br02",
"to": "iw04",
"type": "standard"
},
{
"id": "c-br03-br04",
"from": "br03",
"to": "br04",
"type": "neutral"
},
{
"id": "c-br03-bw01",
"from": "br03",
"to": "bw01",
"type": "standard"
},
{
"id": "c-br03-bw02",
"from": "br03",
"to": "bw02",
"type": "neutral"
},
{
"id": "c-br04-br05",
"from": "br04",
"to": "br05",
"type": "neutral"
},
{
"id": "c-br04-br06",
"from": "br04",
"to": "br06",
"type": "standard"
},
{
"id": "c-br04-bw10",
"from": "br04",
"to": "bw10",
"type": "standard"
},
{
"id": "c-br04-iw03",
"from": "br04",
"to": "iw03",
"type": "neutral"
},
{
"id": "c-br06-bw10",
"from": "br06",
"to": "bw10",
"type": "neutral"
},
{
"id": "c-bw01-bw02",
"from": "bw01",
"to": "bw02",
"type": "neutral"
},
{
"id": "c-bw01-bw03",
"from": "bw01",
"to": "bw03",
"type": "standard"
},
{
"id": "c-bw02-bw03",
"from": "bw02",
"to": "bw03",
"type": "neutral"
},
{
"id": "c-bw02-bw04",
"from": "bw02",
"to": "bw04",
"type": "neutral"
},
{
"id": "c-bw02-ew03",
"from": "bw02",
"to": "ew03",
"type": "neutral"
},
{
"id": "c-bw03-bw04",
"from": "bw03",
"to": "bw04",
"type": "neutral"
},
{
"id": "c-bw03-rh03",
"from": "bw03",
"to": "rh03",
"type": "standard"
},
{
"id": "c-bw04-ew03",
"from": "bw04",
"to": "ew03",
"type": "neutral"
},
{
"id": "c-bw04-rh03",
"from": "bw04",
"to": "rh03",
"type": "standard"
},
{
"id": "c-bw04-rh05",
"from": "bw04",
"to": "rh05",
"type": "neutral"
},
{
"id": "c-bw05-bw06",
"from": "bw05",
"to": "bw06",
"type": "neutral"
},
{
"id": "c-bw05-bw07",
"from": "bw05",
"to": "bw07",
"type": "neutral"
},
{
"id": "c-bw05-bw11",
"from": "bw05",
"to": "bw11",
"type": "neutral"
},
{
"id": "c-bw05-ku04",
"from": "bw05",
"to": "ku04",
"type": "standard"
},
{
"id": "c-bw05-ku06",
"from": "bw05",
"to": "ku06",
"type": "neutral"
},
{
"id": "c-bw05-rh01",
"from": "bw05",
"to": "rh01",
"type": "neutral"
},
{
"id": "c-bw05-rh04",
"from": "bw05",
"to": "rh04",
"type": "standard"
},
{
"id": "c-bw06-bw07",
"from": "bw06",
"to": "bw07",
"type": "neutral"
},
{
"id": "c-bw06-ew04",
"from": "bw06",
"to": "ew04",
"type": "neutral"
},
{
"id": "c-bw07-ew02",
"from": "bw07",
"to": "ew02",
"type": "neutral"
},
{
"id": "c-bw07-ku04",
"from": "bw07",
"to": "ku04",
"type": "standard"
},
{
"id": "c-bw08-bw09",
"from": "bw08",
"to": "bw09",
"type": "neutral"
},
{
"id": "c-bw08-bw10",
"from": "bw08",
"to": "bw10",
"type": "standard"
},
{
"id": "c-bw08-ew01",
"from": "bw08",
"to": "ew01",
"type": "neutral"
},
{
"id": "c-bw08-ku03",
"from": "bw08",
"to": "ku03",
"type": "neutral"
},
{
"id": "c-bw09-bw10",
"from": "bw09",
"to": "bw10",
"type": "standard"
},
{
"id": "c-bw09-ku03",
"from": "bw09",
"to": "ku03",
"type": "standard"
},
{
"id": "c-bw11-li05",
"from": "bw11",
"to": "li05",
"type": "neutral"
},
{
"id": "c-ew01-hi01",
"from": "ew01",
"to": "hi01",
"type": "neutral"
},
{
"id": "c-ew01-ku06",
"from": "ew01",
"to": "ku06",
"type": "neutral"
},
{
"id": "c-ew02-hi01",
"from": "ew02",
"to": "hi01",
"type": "neutral"
},
{
"id": "c-ew03-ew04",
"from": "ew03",
"to": "ew04",
"type": "neutral"
},
{
"id": "c-ew03-hi02",
"from": "ew03",
"to": "hi02",
"type": "neutral"
},
{
"id": "c-ew03-rh05",
"from": "ew03",
"to": "rh05",
"type": "neutral"
},
{
"id": "c-ew03-st05",
"from": "ew03",
"to": "st05",
"type": "neutral"
},
{
"id": "c-ew04-hi01",
"from": "ew04",
"to": "hi01",
"type": "neutral"
},
{
"id": "c-ew04-hi02",
"from": "ew04",
"to": "hi02",
"type": "neutral"
},
{
"id": "c-ew05-hi01",
"from": "ew05",
"to": "hi01",
"type": "neutral"
},
{
"id": "c-ew05-st04",
"from": "ew05",
"to": "st04",
"type": "neutral"
},
{
"id": "c-ew06-hi02",
"from": "ew06",
"to": "hi02",
"type": "neutral"
},
{
"id": "c-ew06-st04",
"from": "ew06",
"to": "st04",
"type": "standard"
},
{
"id": "c-iw01-iw02",
"from": "iw01",
"to": "iw02",
"type": "neutral"
},
{
"id": "c-iw01-li04",
"from": "iw01",
"to": "li04",
"type": "standard"
},
{
"id": "c-iw01-rh02",
"from": "iw01",
"to": "rh02",
"type": "standard"
},
{
"id": "c-iw02-li04",
"from": "iw02",
"to": "li04",
"type": "standard"
},
{
"id": "c-iw02-rh02",
"from": "iw02",
"to": "rh02",
"type": "standard"
},
{
"id": "c-iw03-iw04",
"from": "iw03",
"to": "iw04",
"type": "neutral"
},
{
"id": "c-iw03-li01",
"from": "iw03",
"to": "li01",
"type": "standard"
},
{
"id": "c-iw03-li02",
"from": "iw03",
"to": "li02",
"type": "standard"
},
{
"id": "c-iw04-li02",
"from": "iw04",
"to": "li02",
"type": "standard"
},
{
"id": "c-iw04-li06",
"from": "iw04",
"to": "li06",
"type": "neutral"
},
{
"id": "c-iw05-iw06",
"from": "iw05",
"to": "iw06",
"type": "neutral"
},
{
"id": "c-iw05-ku02",
"from": "iw05",
"to": "ku02",
"type": "standard"
},
{
"id": "c-iw05-li03",
"from": "iw05",
"to": "li03",
"type": "standard"
},
{
"id": "c-iw06-ku02",
"from": "iw06",
"to": "ku02",
"type": "standard"
},
{
"id": "c-iw06-li03",
"from": "iw06",
"to": "li03",
"type": "standard"
},
{
"id": "c-ku01-ku02",
"from": "ku01",
"to": "ku02",
"type": "standard"
},
{
"id": "c-ku01-ku03",
"from": "ku01",
"to": "ku03",
"type": "standard"
},
{
"id": "c-ku01-ku04",
"from": "ku01",
"to": "ku04",
"type": "standard"
},
{
"id": "c-ku01-ku05",
"from": "ku01",
"to": "ku05",
"type": "standard"
},
{
"id": "c-ku02-ku03",
"from": "ku02",
"to": "ku03",
"type": "neutral"
},
{
"id": "c-ku03-ku05",
"from": "ku03",
"to": "ku05",
"type": "neutral"
},
{
"id": "c-ku04-ku06",
"from": "ku04",
"to": "ku06",
"type": "neutral"
},
{
"id": "c-ku05-ku06",
"from": "ku05",
"to": "ku06",
"type": "neutral"
},
{
"id": "c-ku05-ku07",
"from": "ku05",
"to": "ku07",
"type": "neutral"
},
{
"id": "c-ku06-ku07",
"from": "ku06",
"to": "ku07",
"type": "neutral"
},
{
"id": "c-li01-li02",
"from": "li01",
"to": "li02",
"type": "standard"
},
{
"id": "c-li01-li03",
"from": "li01",
"to": "li03",
"type": "standard"
},
{
"id": "c-li01-li04",
"from": "li01",
"to": "li04",
"type": "standard"
},
{
"id": "c-li01-li05",
"from": "li01",
"to": "li05",
"type": "standard"
},
{
"id": "c-li01-li06",
"from": "li01",
"to": "li06",
"type": "neutral"
},
{
"id": "c-li02-li04",
"from": "li02",
"to": "li04",
"type": "neutral"
},
{
"id": "c-li02-li06",
"from": "li02",
"to": "li06",
"type": "standard"
},
{
"id": "c-li06-li07",
"from": "li06",
"to": "li07",
"type": "neutral"
},
{
"id": "c-rh01-rh02",
"from": "rh01",
"to": "rh02",
"type": "standard"
},
{
"id": "c-rh01-rh03",
"from": "rh01",
"to": "rh03",
"type": "standard"
},
{
"id": "c-rh01-rh04",
"from": "rh01",
"to": "rh04",
"type": "standard"
},
{
"id": "c-rh01-rh05",
"from": "rh01",
"to": "rh05",
"type": "standard"
},
{
"id": "c-rh02-rh04",
"from": "rh02",
"to": "rh04",
"type": "neutral"
},
{
"id": "c-rh03-rh05",
"from": "rh03",
"to": "rh05",
"type": "neutral"
},
{
"id": "c-rh04-rh05",
"from": "rh04",
"to": "rh05",
"type": "neutral"
},
{
"id": "c-st01-st02",
"from": "st01",
"to": "st02",
"type": "neutral"
},
{
"id": "c-st01-st03",
"from": "st01",
"to": "st03",
"type": "standard"
},
{
"id": "c-st02-st02c",
"from": "st02",
"to": "st02c",
"type": "standard"
},
{
"id": "c-st03-st03b",
"from": "st03",
"to": "st03b",
"type": "standard"
}
]
}
</script>
<script>
const svg = document.getElementById("map");
const gridLayer = document.getElementById("gridLayer");
const sectorBorder = document.querySelector(".sector-border");
const systemLayer = document.getElementById("systems");
const connectionLayer = document.getElementById("connections");
const connectionHitLayer = document.getElementById("connectionHits");
const toolButtons = Array.from(document.querySelectorAll(".tool-button"));
const statusEl = document.getElementById("status");
const selectionMeta = document.getElementById("selectionMeta");
const systemEditor = document.getElementById("systemEditor");
const connectionEditor = document.getElementById("connectionEditor");
const systemName = document.getElementById("systemName");
const systemColor = document.getElementById("systemColor");
const systemNote = document.getElementById("systemNote");
const connectionType = document.getElementById("connectionType");
const deleteSelected = document.getElementById("deleteSelected");
const backgroundColor = document.getElementById("backgroundColor");
const zoomIn = document.getElementById("zoomIn");
const zoomOut = document.getElementById("zoomOut");
const resetZoom = document.getElementById("resetZoom");
const exportJson = document.getElementById("exportJson");
const importJson = document.getElementById("importJson");
const saveRemote = document.getElementById("saveRemote");
const loadRemote = document.getElementById("loadRemote");
const dataBox = document.getElementById("dataBox");
const resetMap = document.getElementById("resetMap");
const centerSelection = document.getElementById("centerSelection");
const storageKey = "editable-star-map-v1";
const initialData = JSON.parse(document.getElementById("initial-map-data").textContent);
let state = loadState();
let mode = "select";
let selected = null;
let pendingConnectionFrom = null;
let drag = null;
let suppressClick = false;
let currentViewBox = null;
function clone(value) {
return JSON.parse(JSON.stringify(value));
}
function loadState() {
try {
const saved = localStorage.getItem(storageKey);
if (saved) {
return JSON.parse(saved);
}
} catch (error) {
console.warn("Could not load saved map", error);
}
return clone(initialData);
}
function saveState() {
localStorage.setItem(storageKey, JSON.stringify(state));
}
function canUseServerStorage() {
return location.protocol === "http:" || location.protocol === "https:";
}
function validateMapData(next) {
if (!next || !Array.isArray(next.systems) || !Array.isArray(next.connections)) {
throw new Error("systems/connections fehlen");
}
return {
source: next.source,
background: next.background || "#05080f",
grid: next.grid,
viewBox: next.viewBox,
systems: next.systems,
connections: next.connections
};
}
function mapBounds() {
const viewBox = state.viewBox || {};
return {
x: Number.isFinite(viewBox.x) ? viewBox.x : 0,
y: Number.isFinite(viewBox.y) ? viewBox.y : 0,
width: Number.isFinite(viewBox.width) ? viewBox.width : 1174,
height: Number.isFinite(viewBox.height) ? viewBox.height : 1061
};
}
function clampViewBox(viewBox) {
const bounds = mapBounds();
const width = Math.max(160, Math.min(bounds.width, viewBox.width));
const height = Math.max(160, Math.min(bounds.height, viewBox.height));
return {
x: Math.max(bounds.x, Math.min(bounds.x + bounds.width - width, viewBox.x)),
y: Math.max(bounds.y, Math.min(bounds.y + bounds.height - height, viewBox.y)),
width,
height
};
}
function setMapViewBox(viewBox) {
currentViewBox = clampViewBox(viewBox);
svg.setAttribute("viewBox", `${currentViewBox.x} ${currentViewBox.y} ${currentViewBox.width} ${currentViewBox.height}`);
}
function resetMapViewBox() {
setMapViewBox(mapBounds());
}
function zoomMap(factor, centerPoint) {
if (!currentViewBox) {
resetMapViewBox();
}
const center = centerPoint || {
x: currentViewBox.x + currentViewBox.width / 2,
y: currentViewBox.y + currentViewBox.height / 2
};
const nextWidth = currentViewBox.width * factor;
const nextHeight = currentViewBox.height * factor;
const scaleX = nextWidth / currentViewBox.width;
const scaleY = nextHeight / currentViewBox.height;
setMapViewBox({
x: center.x - (center.x - currentViewBox.x) * scaleX,
y: center.y - (center.y - currentViewBox.y) * scaleY,
width: nextWidth,
height: nextHeight
});
}
function gridToPoint(gridX, gridY) {
const grid = state.grid;
if (!grid) {
return { x: gridX, y: gridY };
}
return {
x: grid.margin + (gridX - grid.minCol) * grid.cellSize,
y: grid.margin + (gridY - grid.minRow) * grid.cellSize
};
}
function snapPointToGrid(point) {
const grid = state.grid;
if (!grid) {
return point;
}
const gridX = Math.max(grid.minCol, Math.min(grid.maxCol, Math.round((point.x - grid.margin) / grid.cellSize + grid.minCol)));
const gridY = Math.max(grid.minRow, Math.min(grid.maxRow, Math.round((point.y - grid.margin) / grid.cellSize + grid.minRow)));
return { ...gridToPoint(gridX, gridY), gridX, gridY };
}
function clampPoint(point) {
const bounds = mapBounds();
return {
...point,
x: Math.max(bounds.x, Math.min(bounds.x + bounds.width, Math.round(point.x))),
y: Math.max(bounds.y, Math.min(bounds.y + bounds.height, Math.round(point.y)))
};
}
function findSystem(id) {
return state.systems.find((system) => system.id === id);
}
function findConnection(id) {
return state.connections.find((connection) => connection.id === id);
}
function setStatus(message) {
statusEl.textContent = message;
}
function svgPointFromEvent(event) {
const point = svg.createSVGPoint();
point.x = event.clientX;
point.y = event.clientY;
const transformed = point.matrixTransform(svg.getScreenCTM().inverse());
return clampPoint({ x: transformed.x, y: transformed.y });
}
function setMode(nextMode) {
mode = nextMode;
pendingConnectionFrom = null;
toolButtons.forEach((button) => {
button.classList.toggle("is-active", button.dataset.mode === mode);
});
if (mode === "add") {
setStatus("Punktmodus: freie Stelle anklicken.");
} else if (mode === "connect") {
setStatus("Verbindungsmodus: erstes System anklicken.");
} else {
setStatus("Auswahlmodus.");
}
render();
}
function makeSystemId(name) {
const base = name.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "") || "system";
let id = base;
let index = 2;
while (findSystem(id)) {
id = `${base}-${index}`;
index += 1;
}
return id;
}
function makeConnectionId(from, to) {
const base = `c-${from}-${to}`;
let id = base;
let index = 2;
while (findConnection(id)) {
id = `${base}-${index}`;
index += 1;
}
return id;
}
function createSystem(x, y) {
const number = state.systems.length + 1;
const point = snapPointToGrid({ x, y });
const system = {
id: makeSystemId(`Neues System ${number}`),
name: `Neues System ${number}`,
x: point.x,
y: point.y,
gridX: point.gridX,
gridY: point.gridY,
color: "#d9f0ff",
note: ""
};
state.systems.push(system);
selected = { kind: "system", id: system.id };
saveState();
render();
updateInspector();
setStatus(`${system.name} erstellt.`);
systemName.focus();
systemName.select();
return system;
}
function createConnection(from, to, type = "standard") {
if (from === to) {
setStatus("Ein System kann nicht mit sich selbst verbunden werden.");
return null;
}
const exists = state.connections.some((connection) => {
return (connection.from === from && connection.to === to) ||
(connection.from === to && connection.to === from);
});
if (exists) {
setStatus("Diese Verbindung existiert bereits.");
return null;
}
const connection = {
id: makeConnectionId(from, to),
from,
to,
type
};
state.connections.push(connection);
selected = { kind: "connection", id: connection.id };
pendingConnectionFrom = null;
saveState();
render();
updateInspector();
setStatus("Verbindung erstellt.");
return connection;
}
function deleteSelection() {
if (!selected) {
setStatus("Nichts ausgewaehlt.");
return;
}
if (selected.kind === "system") {
state.systems = state.systems.filter((system) => system.id !== selected.id);
state.connections = state.connections.filter((connection) => {
return connection.from !== selected.id && connection.to !== selected.id;
});
setStatus("System und zugehoerige Verbindungen geloescht.");
} else {
state.connections = state.connections.filter((connection) => connection.id !== selected.id);
setStatus("Verbindung geloescht.");
}
selected = null;
pendingConnectionFrom = null;
saveState();
render();
updateInspector();
}
function selectSystem(id) {
if (mode === "connect") {
if (!pendingConnectionFrom) {
pendingConnectionFrom = id;
selected = { kind: "system", id };
setStatus("Jetzt zweites System anklicken.");
render();
updateInspector();
return;
}
createConnection(pendingConnectionFrom, id);
return;
}
selected = { kind: "system", id };
pendingConnectionFrom = null;
render();
updateInspector();
}
function selectConnection(id) {
selected = { kind: "connection", id };
pendingConnectionFrom = null;
render();
updateInspector();
}
function clearSelection() {
selected = null;
pendingConnectionFrom = null;
render();
updateInspector();
}
function renderConnections() {
connectionLayer.textContent = "";
connectionHitLayer.textContent = "";
for (const connection of state.connections) {
const from = findSystem(connection.from);
const to = findSystem(connection.to);
if (!from || !to) {
continue;
}
const hit = document.createElementNS("http://www.w3.org/2000/svg", "line");
hit.setAttribute("x1", from.x);
hit.setAttribute("y1", from.y);
hit.setAttribute("x2", to.x);
hit.setAttribute("y2", to.y);
hit.setAttribute("class", "connection-hit");
hit.dataset.id = connection.id;
hit.addEventListener("click", (event) => {
event.stopPropagation();
selectConnection(connection.id);
});
connectionHitLayer.appendChild(hit);
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
line.setAttribute("x1", from.x);
line.setAttribute("y1", from.y);
line.setAttribute("x2", to.x);
line.setAttribute("y2", to.y);
line.setAttribute("class", `connection ${connection.type || "standard"}`);
if (selected?.kind === "connection" && selected.id === connection.id) {
line.classList.add("is-selected");
}
connectionLayer.appendChild(line);
}
}
function beginSystemDrag(system, event) {
if (mode !== "select") {
return;
}
event.preventDefault();
event.stopPropagation();
const point = svgPointFromEvent(event);
drag = {
id: system.id,
startX: point.x,
startY: point.y,
systemX: system.x,
systemY: system.y,
pointerId: event.pointerId,
moved: false
};
selected = { kind: "system", id: system.id };
render();
updateInspector();
}
function handleDragMove(event) {
if (!drag) {
return;
}
const system = findSystem(drag.id);
if (!system) {
drag = null;
return;
}
event.preventDefault();
const point = svgPointFromEvent(event);
const dx = point.x - drag.startX;
const dy = point.y - drag.startY;
if (Math.abs(dx) + Math.abs(dy) > 2) {
drag.moved = true;
suppressClick = true;
}
const snappedPoint = snapPointToGrid(clampPoint({
x: drag.systemX + dx,
y: drag.systemY + dy
}));
system.x = snappedPoint.x;
system.y = snappedPoint.y;
system.gridX = snappedPoint.gridX;
system.gridY = snappedPoint.gridY;
render();
}
function handleDragEnd(event) {
if (!drag) {
return;
}
event.preventDefault();
const system = findSystem(drag.id);
if (system && drag.moved) {
selected = { kind: "system", id: system.id };
saveState();
updateInspector();
setStatus(`${system.name} verschoben.`);
}
drag = null;
window.setTimeout(() => {
suppressClick = false;
}, 0);
}
function stopLabelEvent(event) {
event.stopPropagation();
event.preventDefault();
}
function renderSystems() {
systemLayer.textContent = "";
for (const system of state.systems) {
const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
group.setAttribute("class", "system");
group.setAttribute("transform", `translate(${system.x} ${system.y})`);
group.dataset.id = system.id;
if (selected?.kind === "system" && selected.id === system.id) {
group.classList.add("is-selected");
}
if (pendingConnectionFrom === system.id) {
group.classList.add("is-pending");
}
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
circle.setAttribute("r", "8");
circle.setAttribute("class", "system-core");
circle.style.fill = system.color || "#d9f0ff";
const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
label.setAttribute("class", "system-label");
label.setAttribute("y", "-22");
label.textContent = system.name;
label.addEventListener("pointerdown", stopLabelEvent);
label.addEventListener("click", stopLabelEvent);
label.addEventListener("dblclick", stopLabelEvent);
const hit = document.createElementNS("http://www.w3.org/2000/svg", "circle");
hit.setAttribute("r", "24");
hit.setAttribute("class", "system-hit");
group.append(hit, circle, label);
group.addEventListener("pointerdown", (event) => {
beginSystemDrag(system, event);
});
group.addEventListener("click", (event) => {
event.stopPropagation();
if (suppressClick) {
return;
}
selectSystem(system.id);
});
systemLayer.appendChild(group);
}
}
function render() {
if (!currentViewBox) {
currentViewBox = mapBounds();
}
svg.setAttribute("viewBox", `${currentViewBox.x} ${currentViewBox.y} ${currentViewBox.width} ${currentViewBox.height}`);
document.documentElement.style.setProperty("--map-bg", state.background || "#05080f");
backgroundColor.value = state.background || "#05080f";
renderGrid();
renderConnections();
renderSystems();
}
function renderGrid() {
gridLayer.textContent = "";
const bounds = mapBounds();
sectorBorder.setAttribute("x", bounds.x + 8);
sectorBorder.setAttribute("y", bounds.y + 8);
sectorBorder.setAttribute("width", Math.max(0, bounds.width - 16));
sectorBorder.setAttribute("height", Math.max(0, bounds.height - 16));
const grid = state.grid;
if (!grid) {
return;
}
for (let col = grid.minCol; col <= grid.maxCol; col += 1) {
const point = gridToPoint(col, grid.minRow);
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
line.setAttribute("x1", point.x);
line.setAttribute("y1", bounds.y + 20);
line.setAttribute("x2", point.x);
line.setAttribute("y2", bounds.y + bounds.height - 20);
line.setAttribute("class", "grid-line");
gridLayer.appendChild(line);
const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
label.setAttribute("x", point.x);
label.setAttribute("y", bounds.y + 32);
label.setAttribute("class", "grid-label");
label.textContent = col;
gridLayer.appendChild(label);
}
for (let row = grid.minRow; row <= grid.maxRow; row += 1) {
const point = gridToPoint(grid.minCol, row);
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
line.setAttribute("x1", bounds.x + 20);
line.setAttribute("y1", point.y);
line.setAttribute("x2", bounds.x + bounds.width - 20);
line.setAttribute("y2", point.y);
line.setAttribute("class", "grid-line");
gridLayer.appendChild(line);
const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
label.setAttribute("x", bounds.x + 32);
label.setAttribute("y", point.y + 4);
label.setAttribute("class", "grid-label");
label.textContent = row;
gridLayer.appendChild(label);
}
}
function updateInspector() {
systemEditor.classList.add("hidden");
connectionEditor.classList.add("hidden");
if (!selected) {
selectionMeta.textContent = "Kein Objekt ausgewaehlt.";
return;
}
if (selected.kind === "system") {
const system = findSystem(selected.id);
if (!system) {
clearSelection();
return;
}
const gridInfo = Number.isFinite(system.gridX) && Number.isFinite(system.gridY)
? ` Raster ${system.gridX}, ${system.gridY}`
: ` ${system.x}, ${system.y}`;
selectionMeta.textContent = `System: ${system.name} (${gridInfo})`;
systemName.value = system.name;
systemColor.value = system.color || "#d9f0ff";
systemNote.value = system.note || "";
systemEditor.classList.remove("hidden");
}
if (selected.kind === "connection") {
const connection = findConnection(selected.id);
if (!connection) {
clearSelection();
return;
}
const from = findSystem(connection.from);
const to = findSystem(connection.to);
selectionMeta.textContent = `Verbindung: ${from?.name || connection.from} -> ${to?.name || connection.to}`;
connectionType.value = connection.type || "standard";
connectionEditor.classList.remove("hidden");
}
}
function exportMap() {
dataBox.value = JSON.stringify(state, null, 2);
dataBox.focus();
dataBox.select();
setStatus("Karte exportiert.");
}
function importMap() {
try {
state = validateMapData(JSON.parse(dataBox.value));
selected = null;
pendingConnectionFrom = null;
currentViewBox = mapBounds();
saveState();
render();
updateInspector();
setStatus("Karte importiert.");
} catch (error) {
setStatus(`Import fehlgeschlagen: ${error.message}`);
}
}
async function saveRemoteMap() {
if (!canUseServerStorage()) {
setStatus("Server-Speichern braucht den Node-Server.");
exportMap();
return;
}
try {
const response = await fetch("/api/map", {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify(state)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
setStatus("Karte dauerhaft gespeichert.");
} catch (error) {
setStatus(`Speichern fehlgeschlagen: ${error.message}`);
}
}
async function loadRemoteMap(options = {}) {
if (!canUseServerStorage()) {
if (!options.silent) {
setStatus("Server-Laden braucht den Node-Server.");
}
return;
}
try {
const response = await fetch("/api/map", { headers: { accept: "application/json" } });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
state = validateMapData(await response.json());
selected = null;
pendingConnectionFrom = null;
currentViewBox = mapBounds();
saveState();
render();
updateInspector();
if (!options.silent) {
setStatus("Gespeicherte Karte geladen.");
}
} catch (error) {
if (!options.silent) {
setStatus(`Laden fehlgeschlagen: ${error.message}`);
}
}
}
function centerOnSelection() {
if (!selected || selected.kind !== "system") {
setStatus("Zum Zentrieren bitte ein System auswaehlen.");
return;
}
const system = findSystem(selected.id);
if (!system) {
return;
}
const width = 420;
const height = 320;
setMapViewBox({
x: system.x - width / 2,
y: system.y - height / 2,
width,
height
});
setStatus(`${system.name} zentriert. Doppelklick auf Hintergrund setzt Ansicht zurueck.`);
}
toolButtons.forEach((button) => {
button.addEventListener("click", () => setMode(button.dataset.mode));
});
svg.addEventListener("click", (event) => {
if (event.target !== svg) {
return;
}
if (mode === "add") {
const point = svgPointFromEvent(event);
createSystem(point.x, point.y);
return;
}
clearSelection();
});
svg.addEventListener("dblclick", () => {
resetMapViewBox();
setStatus("Ansicht zurueckgesetzt.");
});
systemName.addEventListener("input", () => {
if (selected?.kind !== "system") {
return;
}
const system = findSystem(selected.id);
if (!system) {
return;
}
system.name = systemName.value.trim() || "Unbenannt";
saveState();
render();
updateInspector();
});
systemColor.addEventListener("input", () => {
if (selected?.kind !== "system") {
return;
}
const system = findSystem(selected.id);
if (!system) {
return;
}
system.color = systemColor.value;
saveState();
render();
});
systemNote.addEventListener("input", () => {
if (selected?.kind !== "system") {
return;
}
const system = findSystem(selected.id);
if (!system) {
return;
}
system.note = systemNote.value;
saveState();
});
connectionType.addEventListener("change", () => {
if (selected?.kind !== "connection") {
return;
}
const connection = findConnection(selected.id);
if (!connection) {
return;
}
connection.type = connectionType.value;
saveState();
render();
updateInspector();
});
backgroundColor.addEventListener("input", () => {
state.background = backgroundColor.value;
saveState();
render();
});
zoomIn.addEventListener("click", () => zoomMap(0.78));
zoomOut.addEventListener("click", () => zoomMap(1.28));
resetZoom.addEventListener("click", () => {
resetMapViewBox();
setStatus("Zoom zurueckgesetzt.");
});
deleteSelected.addEventListener("click", deleteSelection);
exportJson.addEventListener("click", exportMap);
importJson.addEventListener("click", importMap);
saveRemote.addEventListener("click", saveRemoteMap);
loadRemote.addEventListener("click", () => loadRemoteMap());
centerSelection.addEventListener("click", centerOnSelection);
resetMap.addEventListener("click", () => {
state = clone(initialData);
selected = null;
pendingConnectionFrom = null;
currentViewBox = mapBounds();
saveState();
render();
updateInspector();
setStatus("Vorlage neu geladen.");
});
window.addEventListener("keydown", (event) => {
if (event.key === "Delete" || event.key === "Backspace") {
const tag = document.activeElement?.tagName;
if (tag === "INPUT" || tag === "TEXTAREA") {
return;
}
deleteSelection();
}
if (event.key === "Escape") {
clearSelection();
setMode("select");
}
});
window.addEventListener("pointermove", handleDragMove);
window.addEventListener("pointerup", handleDragEnd);
window.addEventListener("pointercancel", handleDragEnd);
svg.addEventListener("wheel", (event) => {
event.preventDefault();
zoomMap(event.deltaY < 0 ? 0.84 : 1.18, svgPointFromEvent(event));
}, { passive: false });
render();
updateInspector();
loadRemoteMap({ silent: true });
</script>
</body>
</html>