|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
|
|
|
<head> |
|
|
<meta charset="UTF-8" /> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
|
|
<title id="pageTitle">Tarkov Map with friends</title> |
|
|
<link rel="icon" type="image/x-icon" href="/static/favicon.ico" /> |
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css" /> |
|
|
<script src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"></script> |
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" /> |
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" rel="stylesheet"> |
|
|
<link rel="stylesheet" href="/static/style.css" /> |
|
|
<style> |
|
|
|
|
|
.connection-form { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 0.5rem; |
|
|
} |
|
|
|
|
|
.connection-form .form-control { |
|
|
width: 120px; |
|
|
|
|
|
padding: 0.25rem 0.5rem; |
|
|
font-size: 0.875rem; |
|
|
} |
|
|
|
|
|
.connection-form .btn { |
|
|
padding: 0.25rem 0.5rem; |
|
|
font-size: 0.875rem; |
|
|
} |
|
|
|
|
|
|
|
|
#connectionStatus { |
|
|
font-size: 0.8rem; |
|
|
padding: 0.25rem 0.5rem; |
|
|
border-radius: 0.2rem; |
|
|
} |
|
|
|
|
|
.status-connected { |
|
|
background-color: #d4edda; |
|
|
color: #155724; |
|
|
} |
|
|
|
|
|
.status-disconnected { |
|
|
background-color: #f8d7da; |
|
|
color: #721c24; |
|
|
} |
|
|
|
|
|
.status-connecting { |
|
|
background-color: #cce7ff; |
|
|
color: #004085; |
|
|
} |
|
|
|
|
|
|
|
|
@media (max-width: 768px) { |
|
|
.connection-form { |
|
|
flex-direction: column; |
|
|
align-items: flex-start; |
|
|
gap: 0.25rem; |
|
|
} |
|
|
|
|
|
.connection-form .form-control { |
|
|
width: 100%; |
|
|
} |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
|
|
|
<body data-bs-theme="dark"> |
|
|
<nav class="navbar navbar-expand-sm navbar-dark fixed-top"> |
|
|
<div class="container-fluid"> |
|
|
<a class="navbar-brand" href="/">Tarkov Map</a> |
|
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation"> |
|
|
<span class="navbar-toggler-icon"></span> |
|
|
</button> |
|
|
<div class="collapse navbar-collapse" id="navbarNavDropdown"> |
|
|
<ul class="navbar-nav me-auto"> |
|
|
<li class="nav-item"> |
|
|
<a class="nav-link" href="/">Home</a> |
|
|
</li> |
|
|
<li class="nav-item dropdown"> |
|
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false"> |
|
|
Maps |
|
|
</a> |
|
|
<ul class="dropdown-menu"> |
|
|
<li><a href="/map/woods" class="dropdown-item">Woods</a></li> |
|
|
<li><a href="/map/shoreline" class="dropdown-item">Shoreline</a></li> |
|
|
<li><a href="/map/lighthouse" class="dropdown-item">Lighthouse</a></li> |
|
|
<li><a href="/map/customs" class="dropdown-item">Customs</a></li> |
|
|
<li><a href="/map/interchange" class="dropdown-item">Interchange</a></li> |
|
|
<li><a href="/map/streets-of-tarkov" class="dropdown-item">Streets of Tarkov</a></li> |
|
|
<li><a href="/map/ground-zero" class="dropdown-item">Ground Zero</a></li> |
|
|
<li><a href="/map/reserve" class="dropdown-item">Reserve</a></li> |
|
|
<li><a href="/map/factory" class="dropdown-item">Factory</a></li> |
|
|
</ul> |
|
|
</li> |
|
|
</ul> |
|
|
|
|
|
<form id="groupConnectForm" class="d-flex connection-form"> |
|
|
<input type="text" id="groupNameInput" class="form-control" placeholder="Gruppe" aria-label="Gruppenname"> |
|
|
<button class="btn btn-outline-success" type="submit">Verbinden</button> |
|
|
<span id="connectionStatus" class="status-disconnected">Nicht verbunden</span> |
|
|
</form> |
|
|
</div> |
|
|
</div> |
|
|
</nav> |
|
|
<div id="map" style="margin-top: 56px; height: calc(100vh - 56px);"></div> |
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.bundle.min.js"></script> |
|
|
<script src="/static/js/leaflet.controlGroupedlayer.js"></script> |
|
|
<script src="/static/js/leaflet.controlCoordinates.js"></script> |
|
|
<script> |
|
|
|
|
|
const playerMarkers = {} |
|
|
let currentMarker = null |
|
|
const client_id = Date.now() |
|
|
let mapData = null; |
|
|
let wsgroup = ""; |
|
|
let ws = null; |
|
|
|
|
|
|
|
|
const groupConnectForm = document.getElementById('groupConnectForm'); |
|
|
const groupNameInput = document.getElementById('groupNameInput'); |
|
|
const connectionStatus = document.getElementById('connectionStatus'); |
|
|
const pageTitle = document.getElementById('pageTitle'); |
|
|
|
|
|
|
|
|
function updateConnectionStatus(status, message) { |
|
|
connectionStatus.textContent = message; |
|
|
connectionStatus.className = ''; |
|
|
connectionStatus.classList.add('status-' + status); |
|
|
} |
|
|
|
|
|
|
|
|
function connectToWebSocket(groupName) { |
|
|
if (ws) { |
|
|
ws.close(); |
|
|
} |
|
|
|
|
|
updateConnectionStatus('connecting', 'Verbinde...'); |
|
|
|
|
|
try { |
|
|
const ws = new WebSocket(`wss://sebastiankay-eft-group-map-websocket.hf.space/ws`); |
|
|
|
|
|
|
|
|
ws.onopen = function (event) { |
|
|
console.log("WebSocket connected"); |
|
|
updateConnectionStatus('connected', `Verbunden: ${groupName}`); |
|
|
wsgroup = groupName; |
|
|
|
|
|
ws.send(JSON.stringify({ type: "join", group: groupName, client_type: "web" })); |
|
|
}; |
|
|
|
|
|
ws.onmessage = function (event) { |
|
|
console.log("WebSocket message received:", event.data); |
|
|
const data = JSON.parse(event.data); |
|
|
handleWebSocketMessage(data); |
|
|
}; |
|
|
|
|
|
ws.onclose = function (event) { |
|
|
console.log("WebSocket closed"); |
|
|
updateConnectionStatus('disconnected', 'Verbindung getrennt'); |
|
|
|
|
|
}; |
|
|
|
|
|
ws.onerror = function (error) { |
|
|
console.error("WebSocket error:", error); |
|
|
updateConnectionStatus('disconnected', 'Verbindungsfehler'); |
|
|
}; |
|
|
|
|
|
} catch (error) { |
|
|
console.error("Failed to connect to WebSocket:", error); |
|
|
updateConnectionStatus('disconnected', 'Verbindungsfehler'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
groupConnectForm.addEventListener('submit', function (e) { |
|
|
e.preventDefault(); |
|
|
const groupName = groupNameInput.value.trim(); |
|
|
if (groupName) { |
|
|
connectToWebSocket(groupName); |
|
|
} else { |
|
|
updateConnectionStatus('disconnected', 'Gruppenname erforderlich'); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
function handleWebSocketMessage(data) { |
|
|
console.log(data); |
|
|
switch (data.type) { |
|
|
case "coordinates": |
|
|
const parsedData = data.data; |
|
|
localStorage.setItem("last_marker", JSON.stringify(parsedData)); |
|
|
loadLocalData(); |
|
|
break; |
|
|
case "location_map": |
|
|
const map_name = data.data.map.toLowerCase().replaceAll(" ", "-"); |
|
|
localStorage.setItem("last_map_name", map_name); |
|
|
localStorage.removeItem("last_marker"); |
|
|
removeAllMarkers(); |
|
|
if (!location.pathname.includes(map_name)) { |
|
|
location.pathname = `/map/${map_name}`; |
|
|
} else { |
|
|
location.reload(); |
|
|
} |
|
|
break; |
|
|
case "new_rade_data": |
|
|
console.log(data.data); |
|
|
const radeData = JSON.parse(data.data); |
|
|
localStorage.setItem("rade_data", JSON.stringify(radeData)); |
|
|
loadLocalData(); |
|
|
break; |
|
|
case "joined": |
|
|
console.log(`Erfolgreich Gruppe '${data.group}' beigetreten`); |
|
|
updateConnectionStatus('connected', `Verbunden: ${data.group}`); |
|
|
break; |
|
|
case "error": |
|
|
console.error("WebSocket Fehler:", data.message); |
|
|
updateConnectionStatus('disconnected', `Fehler: ${data.message}`); |
|
|
break; |
|
|
case "client_joined": |
|
|
console.log(`Neuer Client (${data.client_type}) ist der Gruppe beigetreten`); |
|
|
|
|
|
break; |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
function removeAllMarkers() { |
|
|
for (const playername in playerMarkers) { |
|
|
if (playerMarkers.hasOwnProperty(playername)) { |
|
|
map.removeLayer(playerMarkers[playername]); |
|
|
} |
|
|
} |
|
|
Object.keys(playerMarkers).forEach(key => delete playerMarkers[key]); |
|
|
console.log("Alle Marker wurden entfernt."); |
|
|
} |
|
|
|
|
|
|
|
|
function loadLocalData() { |
|
|
const map_name = localStorage.getItem("last_map_name"); |
|
|
if (map_name) { |
|
|
if (!location.pathname.includes(map_name)) { |
|
|
localStorage.removeItem("last_marker"); |
|
|
} |
|
|
} else { |
|
|
localStorage.removeItem("last_marker"); |
|
|
} |
|
|
const markerData = localStorage.getItem("last_marker"); |
|
|
if (markerData) { |
|
|
const parsedData = JSON.parse(markerData); |
|
|
|
|
|
addMarker( |
|
|
parsedData.x, |
|
|
parsedData.y, |
|
|
parsedData.z, |
|
|
parsedData.timestamp, |
|
|
parsedData.preview || false, |
|
|
parsedData.actualmap || "Unbekannte Map", |
|
|
parsedData.playername || "Unnamed Player", |
|
|
parsedData.markercolor || false |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function loadMapData() { |
|
|
try { |
|
|
|
|
|
const pathParts = window.location.pathname.split('/'); |
|
|
const mapName = pathParts[pathParts.length - 1]; |
|
|
|
|
|
if (!mapName) { |
|
|
console.error("Kein Kartenname in der URL gefunden"); |
|
|
return; |
|
|
} |
|
|
|
|
|
const response = await fetch(`/api/map/${mapName}`); |
|
|
if (!response.ok) { |
|
|
throw new Error(`HTTP error! status: ${response.status}`); |
|
|
} |
|
|
|
|
|
mapData = await response.json(); |
|
|
console.log("Kartendaten geladen:", mapData); |
|
|
|
|
|
|
|
|
if (mapData.name) { |
|
|
pageTitle.textContent = `${mapData.name} | Map with friends`; |
|
|
} |
|
|
|
|
|
|
|
|
initializeMap(); |
|
|
} catch (error) { |
|
|
console.error("Fehler beim Laden der Kartendaten:", error); |
|
|
updateConnectionStatus('disconnected', 'Fehler beim Laden der Kartendaten'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const images = { |
|
|
'container_bank-cash-register': 'container_cash-register', |
|
|
'container_bank-safe': 'container_safe', |
|
|
'container_buried-barrel-cache': 'container_buried-barrel-cache', |
|
|
'container_cash-register': 'container_cash-register', |
|
|
'container_cash-register-tar2-2': 'container_cash-register', |
|
|
'container_dead-civilian': 'container_dead-scav', |
|
|
'container_dead-scav': 'container_dead-scav', |
|
|
'container_festive-airdrop-supply-crate': 'container_festive-airdrop-supply-crate', |
|
|
'container_pmc-body': 'container_dead-scav', |
|
|
'container_civilian-body': 'container_dead-scav', |
|
|
'container_drawer': 'container_drawer', |
|
|
'container_duffle-bag': 'container_duffle-bag', |
|
|
'container_grenade-box': 'container_grenade-box', |
|
|
'container_ground-cache': 'container_ground-cache', |
|
|
'container_jacket': 'container_jacket', |
|
|
'container_lab-technician-body': 'container_dead-scav', |
|
|
'container_medbag-smu06': 'container_medbag-smu06', |
|
|
'container_medcase': 'container_medcase', |
|
|
'container_medical-supply-crate': 'container_crate', |
|
|
'container_pc-block': 'container_pc-block', |
|
|
'container_plastic-suitcase': 'container_plastic-suitcase', |
|
|
'container_ration-supply-crate': 'container_crate', |
|
|
'container_safe': 'container_safe', |
|
|
'container_scav-body': 'container_dead-scav', |
|
|
'container_shturmans-stash': 'container_weapon-box', |
|
|
'container_technical-supply-crate': 'container_crate', |
|
|
'container_toolbox': 'container_toolbox', |
|
|
'container_weapon-box': 'container_weapon-box', |
|
|
'container_wooden-ammo-box': 'container_wooden-ammo-box', |
|
|
'container_wooden-crate': 'container_wooden-crate', |
|
|
'extract_pmc': 'extract_pmc', |
|
|
'extract_scav': 'extract_scav', |
|
|
'extract_shared': 'extract_shared', |
|
|
'extract_transit': 'extract_transit', |
|
|
'hazard': 'hazard', |
|
|
'hazard_mortar': 'hazard_mortar', |
|
|
'hazard_minefield': 'hazard', |
|
|
'hazard_sniper': 'hazard', |
|
|
'key': 'key', |
|
|
'lock': 'lock', |
|
|
'loose_loot': 'loose_loot', |
|
|
'quest_item': 'quest_item', |
|
|
'quest_objective': 'quest_objective', |
|
|
'spawn_sniper_scav': 'spawn_sniper_scav', |
|
|
'spawn_bloodhound': 'spawn_bloodhound', |
|
|
'spawn_boss': 'spawn_boss', |
|
|
'spawn_cultist-priest': 'spawn_cultist-priest', |
|
|
'spawn_pmc': 'spawn_pmc', |
|
|
'spawn_rogue': 'spawn_rogue', |
|
|
'spawn_scav': 'spawn_scav', |
|
|
'stationarygun': 'stationarygun', |
|
|
'switch': 'switch', |
|
|
}; |
|
|
|
|
|
const categories = { |
|
|
'extract_pmc': 'PMC', |
|
|
'extract_shared': 'Shared', |
|
|
'extract_scav': 'Scav', |
|
|
'extract_transit': 'Transit', |
|
|
'spawn_sniper_scav': 'Sniper Scav', |
|
|
'spawn_pmc': 'PMC', |
|
|
'spawn_scav': 'Scav', |
|
|
'spawn_boss': 'Boss', |
|
|
'quest_item': 'Item', |
|
|
'quest_objective': 'Objective', |
|
|
'lock': 'Locks', |
|
|
'lever': 'Lever', |
|
|
'stationarygun': 'Stationary Gun', |
|
|
'switch': 'Switch', |
|
|
'place-names': 'Place Names', |
|
|
}; |
|
|
|
|
|
function getCRS(transform) { |
|
|
let scaleX = 1; |
|
|
let scaleY = 1; |
|
|
let marginX = 0; |
|
|
let marginY = 0; |
|
|
if (transform) { |
|
|
scaleX = transform[0]; |
|
|
scaleY = transform[2] * -1; |
|
|
marginX = transform[1]; |
|
|
marginY = transform[3]; |
|
|
} |
|
|
return L.extend({}, L.CRS.Simple, { |
|
|
transformation: new L.Transformation(scaleX, marginX, scaleY, marginY), |
|
|
projection: L.extend({}, L.Projection.LonLat, { |
|
|
project: latLng => { |
|
|
return L.Projection.LonLat.project(applyRotation(latLng, mapData.coordinateRotation || 0)); |
|
|
}, |
|
|
unproject: point => { |
|
|
return applyRotation(L.Projection.LonLat.unproject(point), (mapData.coordinateRotation || 0) * -1); |
|
|
}, |
|
|
}), |
|
|
}); |
|
|
} |
|
|
|
|
|
function applyRotation(latLng, rotation) { |
|
|
if (!latLng.lng && !latLng.lat) { |
|
|
return L.latLng(0, 0); |
|
|
} |
|
|
if (!rotation) { |
|
|
return latLng; |
|
|
} |
|
|
const angleInRadians = (rotation * Math.PI) / 180; |
|
|
const cosAngle = Math.cos(angleInRadians); |
|
|
const sinAngle = Math.sin(angleInRadians); |
|
|
const { lng: x, lat: y } = latLng; |
|
|
const rotatedX = x * cosAngle - y * sinAngle; |
|
|
const rotatedY = x * sinAngle + y * cosAngle; |
|
|
return L.latLng(rotatedY, rotatedX); |
|
|
} |
|
|
|
|
|
function pos(position) { |
|
|
return [position.z, position.x]; |
|
|
} |
|
|
|
|
|
function addElevation(item, popup) { |
|
|
if (!mapData.showElevation) { |
|
|
return; |
|
|
} |
|
|
const elevationContent = L.DomUtil.create('div', undefined, popup); |
|
|
elevationContent.textContent = `Elevation: ${item.position.y.toFixed(2)}`; |
|
|
if (item.top && item.bottom && item.top !== item.position.y && item.bottom !== item.position.y) { |
|
|
const heightContent = L.DomUtil.create('div', undefined, popup); |
|
|
heightContent.textContent = `Top ${item.top.toFixed(2)}, bottom: ${item.bottom.toFixed(2)}`; |
|
|
} |
|
|
} |
|
|
|
|
|
function markerIsOnLayer(marker, layer) { |
|
|
if (!layer) { |
|
|
return true; |
|
|
} |
|
|
var top = marker.options.top || marker.options.position.y; |
|
|
var bottom = marker.options.bottom || marker.options.position.y; |
|
|
for (const extent of layer.options.extents) { |
|
|
if (top >= extent.height[0] && bottom < extent.height[1]) { |
|
|
let containedType = 'partial'; |
|
|
if (bottom >= extent.height[0] && top <= extent.height[1]) { |
|
|
containedType = 'full'; |
|
|
} |
|
|
if (extent.bounds) { |
|
|
for (const boundsArray of extent.bounds) { |
|
|
const bounds = getBounds(boundsArray); |
|
|
if (bounds.contains(pos(marker.options.position))) { |
|
|
return containedType; |
|
|
} |
|
|
} |
|
|
} else { |
|
|
return containedType; |
|
|
} |
|
|
} |
|
|
} |
|
|
return false; |
|
|
} |
|
|
|
|
|
function markerIsOnActiveLayer(marker) { |
|
|
if (!marker.options.position) { |
|
|
return true; |
|
|
} |
|
|
const map = marker._map; |
|
|
|
|
|
const overlays = map.layerControl._layers.map(l => l.layer).filter(l => Boolean(l.options.extents) && l.options.overlay); |
|
|
for (const layer of overlays) { |
|
|
for (const extent of layer.options.extents) { |
|
|
if (markerIsOnLayer(marker, layer) === 'full' && !map.hasLayer(layer) && extent.bounds) { |
|
|
return false; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
const activeOverlay = Object.values(map._layers).find(l => l.options?.extents && l.options?.overlay); |
|
|
if (activeOverlay && markerIsOnLayer(marker, activeOverlay)) { |
|
|
return true; |
|
|
} |
|
|
|
|
|
const baseLayer = Object.values(map._layers).find(l => l.options?.extents && !l.options?.overlay); |
|
|
if (!activeOverlay && markerIsOnLayer(marker, baseLayer)) { |
|
|
return true; |
|
|
} |
|
|
return false; |
|
|
} |
|
|
|
|
|
function checkMarkerForActiveLayers(event) { |
|
|
const marker = event.target || event; |
|
|
const outline = marker.options.outline; |
|
|
const onLevel = markerIsOnActiveLayer(marker); |
|
|
if (onLevel) { |
|
|
marker._icon?.classList.remove('off-level'); |
|
|
if (outline) { |
|
|
outline._path?.classList.remove('off-level'); |
|
|
} |
|
|
} else { |
|
|
marker._icon?.classList.add('off-level'); |
|
|
if (outline) { |
|
|
outline._path?.classList.add('off-level'); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
function activateMarkerLayer(event) { |
|
|
const marker = event.target || event; |
|
|
if (markerIsOnActiveLayer(marker)) { |
|
|
return; |
|
|
} |
|
|
const activeLayers = Object.values(marker._map._layers).filter(l => l.options?.extents && l.options?.overlay); |
|
|
for (const layer of activeLayers) { |
|
|
layer.removeFrom(marker._map); |
|
|
} |
|
|
const heightLayers = marker._map.layerControl._layers.filter(l => l.layer.options.extents && l.layer.options.overlay).map(l => l.layer); |
|
|
for (const layer of heightLayers) { |
|
|
if (markerIsOnLayer(marker, layer)) { |
|
|
layer.addTo(marker._map); |
|
|
break; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
const getALink = (path, contents) => { |
|
|
const a = L.DomUtil.create('a'); |
|
|
a.setAttribute('href', path); |
|
|
a.setAttribute('target', '_blank'); |
|
|
a.append(contents); |
|
|
return a; |
|
|
}; |
|
|
|
|
|
function getScaledBounds(bounds, scaleFactor) { |
|
|
|
|
|
const centerX = (bounds[0][0] + bounds[1][0]) / 2; |
|
|
const centerY = (bounds[0][1] + bounds[1][1]) / 2; |
|
|
|
|
|
const width = bounds[1][0] - bounds[0][0]; |
|
|
const height = bounds[1][1] - bounds[0][1]; |
|
|
const newWidth = width * scaleFactor; |
|
|
const newHeight = height * scaleFactor; |
|
|
|
|
|
const newBounds = [ |
|
|
[centerY - newHeight / 2, centerX - newWidth / 2], |
|
|
[centerY + newHeight / 2, centerX + newWidth / 2] |
|
|
]; |
|
|
return newBounds; |
|
|
} |
|
|
|
|
|
|
|
|
function initializeMap() { |
|
|
if (!mapData) { |
|
|
console.error("Keine Kartendaten verfügbar"); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const map = L.map('map', { |
|
|
maxBounds: getScaledBounds(mapData.svgBounds || mapData.bounds, 1.5), |
|
|
center: [0, 0], |
|
|
zoom: 2, |
|
|
minZoom: mapData.minZoom || 1, |
|
|
maxZoom: mapData.maxZoom || 6, |
|
|
zoomSnap: 0.1, |
|
|
scrollWheelZoom: true, |
|
|
wheelPxPerZoomLevel: 120, |
|
|
crs: getCRS(mapData.transform), |
|
|
attributionControl: false, |
|
|
id: `${mapData.name.replace(/\s+/g, '')}Map`, |
|
|
}); |
|
|
|
|
|
window.map = map; |
|
|
|
|
|
const layerControl = L.control.groupedLayers(null, null, { |
|
|
position: 'topleft', |
|
|
collapsed: true, |
|
|
groupCheckboxes: true, |
|
|
groupsCollapsable: true, |
|
|
exclusiveOptionalGroups: ['Levels'], |
|
|
}).addTo(map); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const bounds = mapData.bounds; |
|
|
const getBounds = (bounds) => { |
|
|
if (!bounds) return undefined; |
|
|
return L.latLngBounds([bounds[0][1], bounds[0][0]], [bounds[1][1], bounds[1][0]]); |
|
|
}; |
|
|
|
|
|
const overlay = L.imageOverlay(mapData.svgPath, getBounds(bounds)); |
|
|
overlay.addTo(map); |
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
function createCustomMarkerIcon(markercolor) { |
|
|
let markerColor; |
|
|
if (markercolor) { |
|
|
markerColor = '#' + markercolor; |
|
|
} else { |
|
|
markerColor = 'currentColor'; |
|
|
} |
|
|
const svgString = ` |
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76" class="marker-svg"> |
|
|
<path d="M60.8 50.5 38 72.9 15.2 50.5 8.1 3.9 38 13.3l29.8-9.4-7 46.6z" style="stroke-linecap:round;stroke-linejoin:round;fill:#fff;stroke:#fff;stroke-width:4.4px"/> |
|
|
<path d="M58.8 49.4 38 69.9 17.1 49.4 10.7 6.9 38 15.5l27.2-8.6-6.4 42.5z" style="fill:${markerColor};stroke:#000;stroke-width:4px;stroke-linecap:round;stroke-linejoin:round"/> |
|
|
</svg> |
|
|
`; |
|
|
|
|
|
return L.divIcon({ |
|
|
html: svgString, |
|
|
className: 'custom-marker', |
|
|
iconSize: [32, 32], |
|
|
iconAnchor: [16, 32], |
|
|
popupAnchor: [0, -32] |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function addMarker(x, y, z, timestamp, preview, actualmap, playername, markercolor) { |
|
|
if (!window.map) { |
|
|
console.error("Karte nicht initialisiert"); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const position = { |
|
|
x: parseFloat(x), |
|
|
y: parseFloat(y), |
|
|
z: parseFloat(z) |
|
|
}; |
|
|
|
|
|
const getBounds = (bounds) => { |
|
|
if (!bounds) return undefined; |
|
|
return L.latLngBounds([bounds[0][1], bounds[0][0]], [bounds[1][1], bounds[1][0]]); |
|
|
}; |
|
|
|
|
|
const positionIsInBounds = (position) => { |
|
|
return getBounds(mapData.bounds).contains(pos(position)); |
|
|
}; |
|
|
|
|
|
if (!positionIsInBounds(position)) { |
|
|
console.error("Position außerhalb der Karte:", position); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const markerIcon = createCustomMarkerIcon(markercolor); |
|
|
const playerMarkerName = playername.toLowerCase().replaceAll(" ", "-"); |
|
|
|
|
|
|
|
|
if (playerMarkers[playerMarkerName]) { |
|
|
window.map.removeLayer(playerMarkers[playerMarkerName]); |
|
|
} |
|
|
|
|
|
|
|
|
playerMarkers[playerMarkerName] = L.marker(pos(position), { |
|
|
icon: markerIcon, |
|
|
position: position, |
|
|
title: `Koordinaten: ${x}, ${y}, ${z}`, |
|
|
riseOnHover: true, |
|
|
zIndexOffset: 400 |
|
|
}); |
|
|
|
|
|
|
|
|
const popupContent = ` |
|
|
<div class="marker-popup"> |
|
|
${playername ? `<strong>Player: ${playername}</strong>` : ''} |
|
|
<strong>Koordinaten:</strong> |
|
|
<span>X: ${x} Y: ${y} Z: ${z}</span> |
|
|
${preview ? `<img class="preview-image" src="${preview}" style="max-width: 100%; height: auto; margin-top: 5px;"><br>` : ''} |
|
|
<small>${timestamp}</small> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
playerMarkers[playerMarkerName].bindPopup(popupContent, { |
|
|
maxWidth: 250, |
|
|
minWidth: 150, |
|
|
autoClose: true, |
|
|
closeOnClick: true |
|
|
}); |
|
|
|
|
|
|
|
|
playerMarkers[playerMarkerName].addTo(window.map); |
|
|
|
|
|
|
|
|
window.map.setView(pos(position), window.map.getZoom(), { |
|
|
animate: true, |
|
|
duration: 0.5 |
|
|
}); |
|
|
|
|
|
console.log("Neuer Marker gesetzt für " + playername + ": " + position); |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function () { |
|
|
loadMapData(); |
|
|
loadLocalData(); |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
|
|
|
</html> |