Änderung: Erweiterung der Taxi-Map-Logik und Verbesserung der Benutzeroberfläche

Änderungen:
- Hinzufügung neuer Modelle für TaxiMapTile, TaxiStreetName und TaxiMapTileStreet zur Unterstützung der Tile- und Straßennamen-Logik.
- Anpassung der TaxiMap- und TaxiMapService-Logik zur Verwaltung von Tiles und Straßennamen.
- Implementierung von Methoden zur Upsert-Logik für Tiles und Straßennamen in der TaxiMapService.
- Verbesserung der Benutzeroberfläche in TaxiToolsView.vue zur Anzeige und Bearbeitung von Straßennamen und zusätzlichen Elementen.

Diese Anpassungen verbessern die Funktionalität und Benutzererfahrung im Taxi-Minispiel erheblich, indem sie eine detailliertere Verwaltung von Karten und Straßennamen ermöglichen.
This commit is contained in:
Torsten Schulz (local)
2025-09-17 18:55:57 +02:00
parent 76fe67fbe3
commit 9db7c88086
20 changed files with 969 additions and 98 deletions

View File

@@ -94,7 +94,7 @@
</div>
<!-- Tile-Palette (rechts) -->
<div class="tile-palette">
<div class="tile-palette tool-panel">
<h4>{{ $t('admin.taxiTools.mapEditor.tilePalette') }}</h4>
<div class="tile-grid">
<div
@@ -113,6 +113,34 @@
</div>
</div>
</div>
<!-- Straßennamen (drittes Panel) -->
<div v-if="showStreetNamePanel" class="tool-panel street-names">
<h4>{{ $t('admin.taxiTools.mapEditor.streetNames') }}</h4>
<div class="panel-body">
<div class="form-group-mini" v-if="supportsHorizontalName">
<label class="arrow-label"></label>
<input type="text" :value="selectedCell?.streetNameH || ''"
@input="onChangeStreetName('h', $event.target.value)" />
</div>
<div class="form-group-mini" v-if="supportsVerticalName">
<label class="arrow-label"></label>
<input type="text" :value="selectedCell?.streetNameV || ''"
@input="onChangeStreetName('v', $event.target.value)" />
</div>
<div class="form-actions-mini" v-if="showContinueOtherButton">
<button type="button" class="btn btn-primary mini" @click="continueStreetNames">
{{ $t('admin.taxiTools.mapEditor.continueOther') }}
</button>
</div>
</div>
</div>
<!-- Zusätzliche Elemente (viertes Panel) -->
<div v-if="showExtraElementsPanel" class="tool-panel extra-elements">
<h4>{{ $t('admin.taxiTools.mapEditor.extraElements') }}</h4>
<div class="panel-body placeholder"></div>
</div>
</div>
</div>
@@ -188,7 +216,7 @@
</div>
<!-- Tile-Palette (rechts) -->
<div class="tile-palette">
<div class="tile-palette tool-panel">
<h4>{{ $t('admin.taxiTools.mapEditor.tilePalette') }}</h4>
<div class="tile-grid">
<div
@@ -207,6 +235,34 @@
</div>
</div>
</div>
<!-- Straßennamen (drittes Panel) -->
<div v-if="showStreetNamePanel" class="tool-panel street-names">
<h4>{{ $t('admin.taxiTools.mapEditor.streetNames') }}</h4>
<div class="panel-body">
<div class="form-group-mini" v-if="supportsHorizontalName">
<label class="arrow-label"></label>
<input type="text" :value="selectedCell?.streetNameH || ''"
@input="onChangeStreetName('h', $event.target.value)" />
</div>
<div class="form-group-mini" v-if="supportsVerticalName">
<label class="arrow-label"></label>
<input type="text" :value="selectedCell?.streetNameV || ''"
@input="onChangeStreetName('v', $event.target.value)" />
</div>
<div class="form-actions-mini" v-if="showContinueOtherButton">
<button type="button" class="btn btn-primary mini" @click="continueStreetNames">
{{ $t('admin.taxiTools.mapEditor.continueOther') }}
</button>
</div>
</div>
</div>
<!-- Zusätzliche Elemente (viertes Panel) -->
<div v-if="showExtraElementsPanel" class="tool-panel extra-elements">
<h4>{{ $t('admin.taxiTools.mapEditor.extraElements') }}</h4>
<div class="panel-body placeholder"></div>
</div>
</div>
</div>
@@ -254,7 +310,7 @@ export default {
mapForm: {
name: '',
description: '',
mapData: []
// mapData entfernt Tiles werden separat gesendet
},
boardCells: {}, // { "x,y": { tileType: "horizontal", x: 0, y: 0 } }
availableTileTypes: [
@@ -286,6 +342,38 @@ export default {
if (Object.keys(this.boardCells).length === 0) return 0;
return this.maxY - this.minY + 1;
},
selectedCell() {
if (!this.selectedCellKey) return null;
return this.boardCells[this.selectedCellKey] || null;
},
showStreetNamePanel() {
// Panel nur anzeigen, wenn Zelle selektiert ist und der Tile-Typ Namen unterstützt
if (!this.selectedCell) return false;
const type = this.selectedCell.tileType;
return this.supportsHorizontal(type) || this.supportsVertical(type);
},
showExtraElementsPanel() {
// Gleiche Logik wie Straßenpanel nur sichtbar, wenn eine Zelle selektiert ist
return !!this.selectedCell;
},
supportsHorizontalName() {
if (!this.selectedCell) return false;
return this.supportsHorizontal(this.selectedCell.tileType);
},
supportsVerticalName() {
if (!this.selectedCell) return false;
return this.supportsVertical(this.selectedCell.tileType);
},
showContinueOtherButton() {
if (!this.selectedCell) return false;
const hAllowed = this.supportsHorizontalName;
const vAllowed = this.supportsVerticalName;
if (!(hAllowed && vAllowed)) return false;
const hasH = !!this.selectedCell.streetNameH;
const hasV = !!this.selectedCell.streetNameV;
// genau einer gesetzt
return (hasH && !hasV) || (!hasH && hasV);
},
minX() {
if (Object.keys(this.boardCells).length === 0) return 0;
@@ -352,6 +440,170 @@ export default {
},
methods: {
supportsHorizontal(tileType) {
// Horizontalen Straßennamen erlauben bei: horizontal, cross, tleft, tright, tup, tdown, corns wo horizontaler Ast existiert
const allowed = ['horizontal', 'cross', 'tleft', 'tright', 'tup', 'tdown', 'cornertopleft', 'cornerbottomleft', 'cornertopright', 'cornerbottomright', 'fuelhorizontal'];
return allowed.includes(tileType);
},
supportsVertical(tileType) {
// Vertikalen Straßennamen erlauben bei: vertical, cross, tup, tdown, etc.
const allowed = ['vertical', 'cross', 'tleft', 'tright', 'tup', 'tdown', 'cornertopleft', 'cornerbottomleft', 'cornertopright', 'cornerbottomright', 'fuelvertical'];
return allowed.includes(tileType);
},
continueStreetNames() {
if (!this.selectedCell) return;
// Wenn nur einer der beiden Namen existiert, kopiere in das andere Feld
const hasH = !!this.selectedCell.streetNameH;
const hasV = !!this.selectedCell.streetNameV;
if (hasH && !hasV) {
const value = this.selectedCell.streetNameH;
this.onChangeStreetName('v', value);
} else if (!hasH && hasV) {
const value = this.selectedCell.streetNameV;
this.onChangeStreetName('h', value);
}
},
onChangeStreetName(dir, value) {
if (!this.selectedCell) return;
const key = dir === 'h' ? 'streetNameH' : 'streetNameV';
// Vue reaktiv: bestehendes Objekt erweitern
const updated = { ...(this.boardCells[this.selectedCellKey] || {}), [key]: value };
this.$set ? this.$set(this.boardCells, this.selectedCellKey, updated) : (this.boardCells[this.selectedCellKey] = updated);
// Namen entlang der Achse bis zu Abschluss-Tiles fortführen
this.propagateStreetNameChain(this.selectedCellKey, dir, value);
},
continueStreetName(dir) {
if (!this.selectedCell) return;
const name = dir === 'h' ? (this.selectedCell.streetNameH || '') : (this.selectedCell.streetNameV || '');
if (!name) return;
this.propagateStreetNameChain(this.selectedCellKey, dir, name);
},
// Führt Namen entlang der Achse fort, bis ein Abschluss-Tile erreicht ist
propagateStreetNameChain(startKey, dir, name) {
const start = this.boardCells[startKey];
if (!start || !start.tileType) return;
const { x, y } = start;
const keyField = dir === 'h' ? 'streetNameH' : 'streetNameV';
const walk = (sx, sy, sign) => {
let cx = sx;
let cy = sy;
while (true) {
if (dir === 'h') cx += (sign === 'right' ? 1 : -1); else cy += (sign === 'down' ? 1 : -1);
const key = `${cx},${cy}`;
const cell = this.boardCells[key];
if (!cell || !cell.tileType) break;
const status = this.classifyNeighbor(dir, sign, cell.tileType);
if (status === 'none') break;
const upd = { ...cell, [keyField]: name };
this.$set ? this.$set(this.boardCells, key, upd) : (this.boardCells[key] = upd);
if (status === 'terminal') break;
}
};
const dirs = this.allowedDirections(start.tileType);
if (dir === 'h') {
if (dirs.right) walk(x, y, 'right');
if (dirs.left) walk(x, y, 'left');
} else {
if (dirs.down) walk(x, y, 'down');
if (dirs.up) walk(x, y, 'up');
}
},
classifyNeighbor(dir, sign, type) {
const isCornerRight = (t) => /corner.*right$/.test(t);
const isCornerLeft = (t) => /corner.*left$/.test(t);
const isCornerTop = (t) => /cornertop.*$/.test(t);
const isCornerBottom= (t) => /cornerbottom.*$/.test(t);
if (dir === 'h') {
// horizontal folgen können: corner<xxx>right, horizontal, tdown, tup, cross, fuelhorizontal, sowie corner<xxx>left und tleft als Abschluss
if (sign === 'right') {
if (isCornerLeft(type) || type === 'tleft') return 'terminal';
if (isCornerRight(type) || ['horizontal','tdown','tup','cross','fuelhorizontal'].includes(type)) return 'continue';
return 'none';
} else { // left
if (isCornerRight(type) || type === 'tright') return 'terminal';
if (isCornerLeft(type) || ['horizontal','tdown','tup','cross','fuelhorizontal'].includes(type)) return 'continue';
return 'none';
}
} else {
// vertikal können folgen: cornerbottom<xxx>, vertical, tleft, tright, cross, fuelvertical, cornerup<xxx>
// Abschluss bei Richtung down: cornerup<xxx> und tup (wir setzen noch, führen aber nicht weiter)
if (sign === 'down') {
if (isCornerTop(type) || type === 'tup') return 'terminal';
if (isCornerBottom(type) || ['vertical','tleft','tright','cross','fuelvertical'].includes(type)) return 'continue';
return 'none';
} else { // up
if (isCornerBottom(type) || type === 'tdown') return 'terminal';
if (isCornerTop(type) || ['vertical','tleft','tright','cross','fuelvertical'].includes(type)) return 'continue';
return 'none';
}
}
},
allowedDirections(tileType) {
// Gibt zurück, in welche Richtungen dieses Tile Anschlüsse hat
switch (tileType) {
case 'cornertopleft': return { left: true, up: true, right: false, down: false };
case 'cornertopright': return { right: true, up: true, left: false, down: false };
case 'cornerbottomleft': return { left: true, down: true, right: false, up: false };
case 'cornerbottomright': return { right: true, down: true, left: false, up: false };
case 'horizontal':
case 'fuelhorizontal': return { left: true, right: true, up: false, down: false };
case 'vertical':
case 'fuelvertical': return { up: true, down: true, left: false, right: false };
case 'cross': return { left: true, right: true, up: true, down: true };
case 'tup': return { up: true, left: true, right: true, down: false };
case 'tdown': return { down: true, left: true, right: true, up: false };
case 'tleft': return { left: true, up: true, down: true, right: false };
case 'tright': return { right: true, up: true, down: true, left: false };
default: return { left: false, right: false, up: false, down: false };
}
},
propagateStreetNameFrom(startKey, dir, name) {
const keyField = dir === 'h' ? 'streetNameH' : 'streetNameV';
const step = dir === 'h' ? { dx: 1, dy: 0 } : { dx: 0, dy: 1 };
const oppositeStep = { dx: -step.dx, dy: -step.dy };
// in positive Richtung fortführen
this.walkAndSet(startKey, step, dir, keyField, name);
// in negative Richtung fortführen
this.walkAndSet(startKey, oppositeStep, dir, keyField, name);
},
walkAndSet(startKey, step, dir, keyField, name) {
const canContinue = (tileType, dir) => {
switch (tileType) {
case 'horizontal': return dir === 'h';
case 'vertical': return dir === 'v';
case 'cross': return true;
case 'fuelhorizontal': return dir === 'h';
case 'fuelvertical': return dir === 'v';
case 'cornertopleft':
case 'cornertopright':
case 'cornerbottomleft':
case 'cornerbottomright':
return false; // Kurve blockt die Fortführung
case 'tup': return dir === 'h';
case 'tdown': return dir === 'h';
case 'tleft': return dir === 'v';
case 'tright': return dir === 'v';
default: return false;
}
};
let [x, y] = startKey.split(',').map(Number);
while (true) {
x += step.dx; y += step.dy;
const key = `${x},${y}`;
const cell = this.boardCells[key];
if (!cell || !cell.tileType) break;
if (!canContinue(cell.tileType, dir)) break;
const updated = { ...cell, [keyField]: name };
this.$set ? this.$set(this.boardCells, key, updated) : (this.boardCells[key] = updated);
}
},
async loadMaps() {
try {
const user = this.$store.getters.user;
@@ -402,11 +654,27 @@ export default {
this.mapForm = {
name: map.name,
description: map.description,
mapData: map.mapData || []
};
if (map.mapData && map.mapData.length > 0) {
this.createBoardFromData(map.mapData);
// Falls Tiles aus Backend mitkommen, Board daraus erstellen
if (Array.isArray(map.tiles) && map.tiles.length > 0) {
this.boardCells = {};
for (const t of map.tiles) {
const key = `${t.x},${t.y}`;
this.boardCells[key] = { tileType: t.tileType, x: t.x, y: t.y };
}
// Straßennamen aus tileStreets übernehmen
if (Array.isArray(map.tileStreets)) {
for (const s of map.tileStreets) {
const key = `${s.x},${s.y}`;
if (this.boardCells[key]) {
this.boardCells[key] = {
...this.boardCells[key],
streetNameH: s.streetNameH?.name || null,
streetNameV: s.streetNameV?.name || null
};
}
}
}
} else {
this.initializeBoard();
}
@@ -433,9 +701,8 @@ export default {
// Wenn eine Position ausgewählt ist, platziere das Tile
if (this.selectedCellKey !== null) {
this.placeTile(this.selectedCellKey, tileType);
// Setze Auswahl zurück nach dem Platzieren
this.selectedCellKey = null;
this.selectedTileType = null;
// Nach dem Setzen bleibt die Zelle ausgewählt
// Tile-Typ-Auswahl bleibt erhalten, um mehrere gleiche Tiles zu setzen
}
},
@@ -453,6 +720,9 @@ export default {
y: y
};
// Nach dem Setzen: Zelle ausgewählt lassen
this.selectedCellKey = cellKey;
// Erweitere das Board um benachbarte leere Zellen falls nötig
this.expandBoardIfNeeded(x, y, tileType);
},
@@ -681,13 +951,19 @@ export default {
async saveMap() {
try {
// Tiles in Array-Form bringen
const tiles = Object.values(this.boardCells)
.filter(c => !!c.tileType)
.map(c => ({ x: c.x, y: c.y, tileType: c.tileType }));
const tileStreetNames = this.collectTileStreetNames();
const mapData = {
...this.mapForm,
width: this.boardWidth,
height: this.boardHeight,
tileSize: 50,
mapTypeId: 1, // Standard Map-Typ
mapData: this.generateMapData()
mapTypeId: 1,
tiles,
tileStreetNames
};
let savedMap;
@@ -718,6 +994,18 @@ export default {
}
},
collectTileStreetNames() {
const result = {};
for (const [key, cell] of Object.entries(this.boardCells)) {
if (!cell.tileType) continue;
const { streetNameH, streetNameV } = cell;
if (streetNameH || streetNameV) {
result[key] = { streetNameH: streetNameH || null, streetNameV: streetNameV || null };
}
}
return result;
},
async deleteMap(mapId) {
if (confirm('Möchtest du diese Map wirklich löschen?')) {
try {
@@ -941,6 +1229,44 @@ export default {
flex: 0 0 300px;
}
/* Vereinheitlichte Tool-Panels rechts */
.tool-panel {
flex: 0 0 220px;
}
.tool-panel h4 {
margin: 0 0 8px 0; /* weniger Abstand oben/unten */
color: #F9A22C;
font-size: 18px;
}
.panel-body.placeholder {
border: 2px dashed #ddd;
border-radius: 8px;
padding: 8px;
color: #999;
text-align: center;
}
.form-group-mini {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.arrow-label {
width: 28px;
text-align: center;
font-weight: 600;
}
.form-group-mini input {
flex: 1;
padding: 6px 8px;
border: 1px solid #ccc;
border-radius: 6px;
}
.btn.mini {
padding: 6px 10px;
font-size: 12px;
}
.tile-palette h4 {
margin-bottom: 15px;
color: #F9A22C;
@@ -953,7 +1279,7 @@ export default {
grid-template-rows: repeat(4, 50px);
gap: 5px;
border: 2px solid #ddd;
padding: 10px;
padding: 6px; /* weniger Innenabstand, damit oben/unten kompakter */
background: #f9f9f9;
border-radius: 8px;
}

View File

@@ -43,6 +43,22 @@
<li>Vermeide Kollisionen mit anderen Fahrzeugen</li>
</ul>
</div>
<!-- Straßenname-Legende -->
<div class="game-objectives" v-if="streetLegend.length">
<h4>Straßennamen</h4>
<ul>
<li
v-for="item in streetLegend"
:key="item.num"
class="legend-street-item"
:class="{ selected: selectedStreetName === item.name }"
@click="onSelectStreet(item.name)"
>
{{ item.num }}: {{ item.name }}
</li>
</ul>
</div>
</div>
<!-- Spiel-Canvas mit Tacho -->
@@ -228,7 +244,40 @@ export default {
keys: {},
motorSound: null,
audioContext: null,
audioUnlockHandler: null
audioUnlockHandler: null,
selectedStreetName: null
}
},
computed: {
toggleIcon() {
return this.isStatsExpanded ? '' : '+';
},
streetLegend() {
// Sammle alle Straßennamen aus currentMap.tileStreets und vergebe laufende Nummern
try {
const legend = [];
if (!this.currentMap || !Array.isArray(this.currentMap.tileStreets)) return legend;
const nameToNum = new Map();
let counter = 1;
for (const ts of this.currentMap.tileStreets) {
if (ts.streetNameH && ts.streetNameH.name) {
const n = ts.streetNameH.name;
if (!nameToNum.has(n)) nameToNum.set(n, counter++);
}
if (ts.streetNameV && ts.streetNameV.name) {
const n = ts.streetNameV.name;
if (!nameToNum.has(n)) nameToNum.set(n, counter++);
}
}
// Sortiert nach zugewiesener Nummer
const entries = Array.from(nameToNum.entries()).sort((a,b) => a[1]-b[1]);
for (const [name, num] of entries) {
legend.push({ num, name });
}
return legend;
} catch (e) {
return [];
}
}
},
async mounted() {
@@ -245,6 +294,74 @@ export default {
this.cleanup();
},
methods: {
// Hilfsfunktion: Liefert laufende Nummer für gegebenen Straßennamen
getStreetNumberByName(name) {
const item = this.streetLegend.find(it => it.name === name);
return item ? item.num : null;
},
// Zeichnet die Straßen-Nr. auf die Minimap, je nach Tile-Typ und Position (pro Name nur einmal)
drawStreetNumbersOnMinimap(ctx, x, y, size, tileType, col, row, drawnNames) {
if (!this.currentMap || !Array.isArray(this.currentMap.tileStreets)) return;
// Finde etwaige StreetName-Einträge für dieses Tile
const entry = this.currentMap.tileStreets.find(ts => ts.x === col && ts.y === row);
if (!entry) return;
ctx.save();
const baseFontSize = Math.max(9, Math.floor(size * 0.22) - 1);
ctx.font = `${baseFontSize}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const left = x + size * 0.2;
const right = x + size * 0.8;
const top = y + size * 0.2;
const bottom = y + size * 0.8;
const hName = entry.streetNameH?.name || null;
const vName = entry.streetNameV?.name || null;
const hNum = hName ? this.getStreetNumberByName(hName) : null;
const vNum = vName ? this.getStreetNumberByName(vName) : null;
const t = tileType;
// Regeln gemäß Vorgabe
// Horizontal linke Seite:
const hLeftTiles = new Set(['cornerbottomleft','cornertopleft','horizontal','tdown','tup','cross','fuelhorizontal','tleft']);
// Horizontal rechte Seite:
const hRightTiles = new Set(['cornerbottomright','cornertopright','tright']);
// Vertical oben:
const vTopTiles = new Set(['cornertopleft','cornertopright','vertical','tup','tleft','tright','cross','fuelvertical']);
// Vertical unten:
const vBottomTiles = new Set(['cornerbottomleft','cornerbottomright','tdown']);
// Zusätzliche Kanten-spezifische Regeln aus der Einleitung
// cornerbottomright: horizontal rechts, vertical unten
// cornerbottomleft: horizontal links, vertical unten
// ... ist bereits über Sets abgedeckt
if (hNum !== null && hName && !drawnNames.has(hName)) {
const hx = hRightTiles.has(t) ? right : left;
const hy = y + size * 0.5 + 1; // 1px nach unten
const isSelH = (this.selectedStreetName && this.selectedStreetName === hName);
ctx.fillStyle = isSelH ? '#ffffff' : '#111';
ctx.font = isSelH ? `bold ${baseFontSize}px sans-serif` : `${baseFontSize}px sans-serif`;
ctx.fillText(String(hNum), hx, hy);
drawnNames.add(hName);
}
if (vNum !== null && vName && !drawnNames.has(vName)) {
const vx = x + size * 0.5;
const vy = vTopTiles.has(t) ? top : (vBottomTiles.has(t) ? bottom : top);
const isSelV = (this.selectedStreetName && this.selectedStreetName === vName);
ctx.fillStyle = isSelV ? '#ffffff' : '#111';
ctx.font = isSelV ? `bold ${baseFontSize}px sans-serif` : `${baseFontSize}px sans-serif`;
ctx.fillText(String(vNum), vx, vy);
drawnNames.add(vName);
}
ctx.restore();
},
// Mapping zwischen Spiel-Tile-Namen (lowercase) und streetCoordinates.json (camelCase)
mapTileTypeToStreetCoordinates(tileType) {
const mapping = {
@@ -256,7 +373,11 @@ export default {
'vertical': 'vertical',
'cross': 'cross',
'fuelhorizontal': 'fuelHorizontal',
'fuelvertical': 'fuelVertical'
'fuelvertical': 'fuelVertical',
'tleft': 'tLeft',
'tright': 'tRight',
'tup': 'tUp',
'tdown': 'tDown'
};
return mapping[tileType] || tileType;
},
@@ -588,9 +709,9 @@ export default {
this.lastSteerChange = currentTime;
}
// Aktualisiere Position (1/4 der ursprünglichen Bewegung)
this.taxi.x += Math.cos(this.taxi.angle) * this.taxi.speed * 0.1;
this.taxi.y += Math.sin(this.taxi.angle) * this.taxi.speed * 0.1;
// Aktualisiere Position (+20% Geschwindigkeit)
this.taxi.x += Math.cos(this.taxi.angle) * this.taxi.speed * 0.12;
this.taxi.y += Math.sin(this.taxi.angle) * this.taxi.speed * 0.12;
// Aktualisiere aktuelle Tile-Position
this.updateCurrentTile();
@@ -995,9 +1116,12 @@ export default {
if (this.tiles.images[tileType]) {
this.ctx.drawImage(this.tiles.images[tileType], 0, 0, tileSize, tileSize);
}
// Straßennummern auf Haupt-Canvas zeichnen
this.drawStreetNumbersOnMainCanvas(this.ctx, tileSize, tileType, this.currentTile.col, this.currentTile.row);
}
}
} else {
}
else {
// Fallback: Zeichne Standard-Tile wenn keine Map geladen ist
const tileType = 'cornertopleft'; // Standard-Tile
const streetTileType = this.mapTileTypeToStreetCoordinates(tileType);
@@ -1009,8 +1133,53 @@ export default {
if (this.tiles.images[tileType]) {
this.ctx.drawImage(this.tiles.images[tileType], 0, 0, tileSize, tileSize);
}
// Straßennummern auch im Fallback dezent zeichnen (Spalten/Zeilen 0)
this.drawStreetNumbersOnMainCanvas(this.ctx, tileSize, tileType, 0, 0);
}
},
drawStreetNumbersOnMainCanvas(ctx, size, tileType, col, row) {
if (!this.currentMap || !Array.isArray(this.currentMap.tileStreets)) return;
const absCol = col + (this.currentMap.offsetX || 0);
const absRow = row + (this.currentMap.offsetY || 0);
const entry = this.currentMap.tileStreets.find(ts => ts.x === absCol && ts.y === absRow);
if (!entry) return;
ctx.save();
ctx.fillStyle = 'rgba(10,10,10,0.8)';
ctx.font = `${Math.max(11, Math.floor(size * 0.06) - 1)}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const left = size * 0.2;
const right = size * 0.8;
const top = size * 0.2;
const bottom = size * 0.8;
const hName = entry.streetNameH?.name || null;
const vName = entry.streetNameV?.name || null;
const hNum = hName ? this.getStreetNumberByName(hName) : null;
const vNum = vName ? this.getStreetNumberByName(vName) : null;
const t = tileType;
const hLeftTiles = new Set(['cornerbottomleft','cornertopleft','horizontal','tdown','tup','cross','fuelhorizontal','tleft']);
const hRightTiles = new Set(['cornerbottomright','cornertopright','tright']);
const vTopTiles = new Set(['cornertopleft','cornertopright','vertical','tup','tleft','tright','cross','fuelvertical']);
const vBottomTiles = new Set(['cornerbottomleft','cornerbottomright','tdown']);
if (hNum !== null) {
const hx = hRightTiles.has(t) ? right : left;
const hy = size * 0.5 + 1; // 1px nach unten
ctx.fillText(String(hNum), hx, hy);
}
if (vNum !== null) {
const vx = size * 0.5;
const vy = vTopTiles.has(t) ? top : (vBottomTiles.has(t) ? bottom : top);
ctx.fillText(String(vNum), vx, vy);
}
ctx.restore();
},
getTileType(row, col, rows, cols) {
// Ecken
@@ -1133,12 +1302,14 @@ export default {
async loadTiles() {
const tileNames = [
'cornerbottomleft', 'cornerbottomright', 'cornertopleft', 'cornertopright',
'cross', 'fuelhorizontal', 'fuelvertical', 'horizontal', 'vertical'
'cross', 'fuelhorizontal', 'fuelvertical', 'horizontal', 'vertical',
'tleft', 'tright', 'tup', 'tdown'
];
const mapTileNames = [
'map-cornerbottomleft', 'map-cornerbottomright', 'map-cornertopleft', 'map-cornertopright',
'map-cross', 'map-fuelhorizontal', 'map-fuelvertical', 'map-horizontal', 'map-vertical'
'map-cross', 'map-fuelhorizontal', 'map-fuelvertical', 'map-horizontal', 'map-vertical',
'map-tleft', 'map-tright', 'map-tup', 'map-tdown'
];
// Lade normale Tiles
@@ -1173,12 +1344,11 @@ export default {
async loadMaps() {
try {
const response = await apiClient.get('/api/taxi-maps/maps');
this.maps = response.data.data || [];
this.maps = (response.data.data || []).map(m => this.buildMapDataFromTiles(m));
// Verwende die erste verfügbare Map als Standard
if (this.maps.length > 0) {
this.currentMap = this.maps[0];
this.selectedMap = this.maps[0]; // Auch selectedMap setzen
this.selectedMapId = this.maps[0].id;
// Canvas-Größe an geladene Map anpassen
@@ -1198,7 +1368,7 @@ export default {
// Wechsle zur ausgewählten Map
const selectedMap = this.maps.find(map => map.id === this.selectedMapId);
if (selectedMap) {
this.currentMap = selectedMap;
this.currentMap = this.buildMapDataFromTiles({ ...selectedMap });
// console.log('Gewechselt zu Map:', selectedMap);
// Canvas-Größe an neue Map anpassen
@@ -1248,6 +1418,9 @@ export default {
const offsetX = (canvas.width - cols * tileSize) / 2;
const offsetY = (canvas.height - rows * tileSize) / 2;
// Set, um pro Straßennamen nur eine Nummer zu zeichnen
const drawnNames = new Set();
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const x = offsetX + col * tileSize;
@@ -1261,6 +1434,14 @@ export default {
if (this.tiles.images[mapTileType]) {
ctx.drawImage(this.tiles.images[mapTileType], x, y, tileSize, tileSize);
}
// Straßensegment der ausgewählten Straße hervorheben
const absCol = col + (this.currentMap.offsetX || 0);
const absRow = row + (this.currentMap.offsetY || 0);
this.drawStreetFillOnMinimap(ctx, x, y, tileSize, tileType, absCol, absRow);
// Straßennamen-Nummern zeichnen, basierend auf tileStreets (nur einmal pro Name)
this.drawStreetNumbersOnMinimap(ctx, x, y, tileSize, tileType, absCol, absRow, drawnNames);
}
}
}
@@ -1289,6 +1470,8 @@ export default {
if (this.tiles.images[mapTileType]) {
ctx.drawImage(this.tiles.images[mapTileType], x, y, tileSize, tileSize);
}
// Keine echten Namen im Fallback
}
}
}
@@ -1336,12 +1519,86 @@ export default {
ctx.moveTo(taxiX, taxiY);
ctx.lineTo(endX, endY);
ctx.stroke();
}
},
computed: {
toggleIcon() {
return this.isStatsExpanded ? '' : '+';
},
buildMapDataFromTiles(map) {
if (!map || !Array.isArray(map.tiles) || map.tiles.length === 0) {
map.mapData = null;
map.offsetX = 0;
map.offsetY = 0;
return map;
}
const xs = map.tiles.map(t => t.x);
const ys = map.tiles.map(t => t.y);
const minX = Math.min(...xs);
const maxX = Math.max(...xs);
const minY = Math.min(...ys);
const maxY = Math.max(...ys);
const width = (maxX - minX + 1) || 1;
const height = (maxY - minY + 1) || 1;
const grid = Array.from({ length: height }, () => Array(width).fill(null));
for (const t of map.tiles) {
const col = t.x - minX;
const row = t.y - minY;
if (row >= 0 && row < height && col >= 0 && col < width) {
grid[row][col] = t.tileType;
}
}
map.mapData = grid;
map.offsetX = minX;
map.offsetY = minY;
return map;
},
onSelectStreet(name) {
this.selectedStreetName = (this.selectedStreetName === name) ? null : name;
},
// Variante B: gesamte Straßenfläche im Tile halbtransparent rot füllen
drawStreetFillOnMinimap(ctx, x, y, size, tileType, col, row) {
if (!this.selectedStreetName || !this.currentMap || !Array.isArray(this.currentMap.tileStreets)) return;
const entry = this.currentMap.tileStreets.find(ts => ts.x === col && ts.y === row);
if (!entry) return;
const wantH = entry.streetNameH?.name === this.selectedStreetName;
const wantV = entry.streetNameV?.name === this.selectedStreetName;
if (!wantH && !wantV) return;
const streetTileType = this.mapTileTypeToStreetCoordinates(tileType);
const regions = streetCoordinates.getDriveableRegions(streetTileType, size);
if (!regions || regions.length === 0) return;
// Banddicke exakt 12px (bei Referenz 50px) skaliert auf aktuelle size
const scale = size / 50;
const bandThickness = 12 * scale; // px
// Clip: Tile-Rechteck MINUS Hindernis-Regionen => übrig bleibt die graue Straßenfläche
ctx.save();
ctx.beginPath();
// 1) volles Tile
ctx.rect(x, y, size, size);
// 2) Hindernis-Regionen hinzufügen (EvenOdd)
for (const region of regions) {
if (!region || region.length === 0) continue;
ctx.moveTo(x + region[0].x, y + region[0].y);
for (let i = 1; i < region.length; i++) {
ctx.lineTo(x + region[i].x, y + region[i].y);
}
ctx.closePath();
}
// EvenOdd erhält Fläche außerhalb der Regionen (Straße)
ctx.clip('evenodd');
ctx.fillStyle = '#ff4d4d';
const marginRight = 1; // rechts 1px kürzen
const marginBottom = 1; // unten 1px kürzen
if (wantH) {
const by = y + size * 0.5 - bandThickness / 2;
ctx.fillRect(x, by, size, bandThickness);
}
if (wantV) {
const bx = x + size * 0.5 - bandThickness / 2;
ctx.fillRect(bx, y, bandThickness, size);
}
ctx.restore();
}
}
}
@@ -1793,4 +2050,11 @@ export default {
height: auto;
}
}
.legend-street-item {
cursor: pointer;
}
.legend-street-item.selected {
font-weight: 700;
}
</style>