|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
|
|
|
<head> |
|
|
<meta charset="UTF-8" /> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
|
|
|
|
|
|
|
|
<title>{{ maps[0].name }} | 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 href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" rel="stylesheet"> |
|
|
|
|
|
|
|
|
<link rel="stylesheet" href="/static/style.css" /> |
|
|
</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"> |
|
|
<li class="nav-item"> |
|
|
<a class="nav-link active" aria-current="page" 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> |
|
|
</div> |
|
|
</div> |
|
|
</nav> |
|
|
|
|
|
<div id="map"></div> |
|
|
|
|
|
|
|
|
<div id="settings-modal" class="modal"> |
|
|
<div class="modal-background"></div> |
|
|
|
|
|
<div class="modal-content container"> |
|
|
<div class="box"> |
|
|
<section class="hero"> |
|
|
<div class="hero-body"> |
|
|
<p class="title has-text-weight-bold">EINSTELLUNGEN</p> |
|
|
<hr class="title-line"> |
|
|
<p class="subtitle">Subtitle</p> |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
<section class="section"> |
|
|
<div class="content"> |
|
|
<p> |
|
|
Hier die Einstellungen basierend auf der config.ini file. |
|
|
|
|
|
{# |
|
|
[mainconfig] |
|
|
eft_log_older_path = E:/Battlestate Games/Escape from Tarkov/Logs/ |
|
|
eft_screenshots_folder_path = C:/Users/Sebastian/Documents/Escape from Tarkov/Screenshots/ |
|
|
delete_screenshots = True |
|
|
eft_show_exits_key = o |
|
|
eft_take_screenshot_key = del #} |
|
|
</p> |
|
|
</div> |
|
|
</section> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<button class="modal-close is-large" aria-label="close"></button> |
|
|
</div> |
|
|
|
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.bundle.min.js"></script> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/js/all.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() |
|
|
const mapData = {{ maps[0] | tojson }} |
|
|
console.info(mapData) |
|
|
|
|
|
const transform = mapData.transform |
|
|
const bounds = mapData.bounds |
|
|
const svgBounds = mapData.svgBounds ? mapData.svgBounds : bounds |
|
|
const coordinateRotation = mapData.coordinateRotation ? mapData.coordinateRotation : 0 |
|
|
const svgPath = mapData.svgPath |
|
|
const imageUrl = mapData.svgPath |
|
|
const minZoom = mapData.minZoom ? mapData.minZoom : 1 |
|
|
const maxZoom = mapData.maxZoom ? mapData.maxZoom : 6 |
|
|
const showElevation = false; |
|
|
const showStaticMarkers = false |
|
|
const wsgroup = "1234" |
|
|
let currentZoom = 3; |
|
|
|
|
|
|
|
|
const ws = new WebSocket(`wss://sebastiankay-eft-group-map-websocket.hf.space/ws`); |
|
|
|
|
|
ws.onmessage = function (event) { |
|
|
console.log(event.data); |
|
|
const data = JSON.parse(event.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; |
|
|
} |
|
|
}; |
|
|
ws.onopen = function (event) { |
|
|
ws.send(JSON.stringify({ type: "join", group: wsgroup })); |
|
|
}; |
|
|
|
|
|
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 |
|
|
); |
|
|
} |
|
|
} |
|
|
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, coordinateRotation)); |
|
|
}, |
|
|
unproject: point => { |
|
|
return applyRotation(L.Projection.LonLat.unproject(point), coordinateRotation * -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 (!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; |
|
|
} |
|
|
|
|
|
|
|
|
const map = L.map('map', { |
|
|
maxBounds: getScaledBounds(svgBounds, 1.5), |
|
|
|
|
|
center: [0, 0], |
|
|
zoom: 2, |
|
|
minZoom: minZoom, |
|
|
maxZoom: maxZoom, |
|
|
zoomSnap: 0.1, |
|
|
scrollWheelZoom: true, |
|
|
wheelPxPerZoomLevel: 120, |
|
|
crs: getCRS(transform), |
|
|
attributionControl: false, |
|
|
id: "wwoodsMap", |
|
|
}); |
|
|
const layerControl = L.control.groupedLayers(null, null, { |
|
|
position: 'topleft', |
|
|
collapsed: true, |
|
|
groupCheckboxes: true, |
|
|
groupsCollapsable: true, |
|
|
exclusiveOptionalGroups: ['Levels'], |
|
|
}).addTo(map); |
|
|
layerControl.on('overlayToggle', (e) => { |
|
|
const layerState = e.detail; |
|
|
if (layerState.checked) { |
|
|
mapViewRef.current.layer = layerState.key; |
|
|
} else { |
|
|
mapViewRef.current.layer = undefined; |
|
|
} |
|
|
}); |
|
|
layerControl.on('layerToggle', (e) => { |
|
|
const layerState = e.detail; |
|
|
if (!layerState.checked) { |
|
|
mapSettingsRef.current.hiddenLayers.push(layerState.key); |
|
|
} else { |
|
|
mapViewRef.current.layer = layerState.key; |
|
|
mapSettingsRef.current.hiddenLayers = mapSettingsRef.current.hiddenLayers.filter(key => key !== layerState.key); |
|
|
} |
|
|
updateSavedMapSettings(); |
|
|
}); |
|
|
layerControl.on('groupToggle', (e) => { |
|
|
const groupState = e.detail; |
|
|
for (const groupLayer of layerControl._layers) { |
|
|
if (groupLayer.group?.key !== groupState.key) { |
|
|
continue; |
|
|
} |
|
|
if (!groupState.checked) { |
|
|
mapSettingsRef.current.hiddenLayers.push(groupLayer.key); |
|
|
} else { |
|
|
mapSettingsRef.current.hiddenLayers = mapSettingsRef.current.hiddenLayers.filter(key => key !== groupLayer.key); |
|
|
} |
|
|
} |
|
|
if (!groupState.checked) { |
|
|
mapSettingsRef.current.hiddenGroups.push(groupState.key); |
|
|
} else { |
|
|
mapSettingsRef.current.hiddenGroups = mapSettingsRef.current.hiddenGroups.filter(key => key !== groupState.key); |
|
|
} |
|
|
updateSavedMapSettings(); |
|
|
}); |
|
|
layerControl.on('groupCollapseToggle', (e) => { |
|
|
const groupState = e.detail; |
|
|
if (groupState.collapsed) { |
|
|
mapSettingsRef.current.collapsedGroups.push(groupState.key); |
|
|
} else { |
|
|
mapSettingsRef.current.collapsedGroups = mapSettingsRef.current.collapsedGroups.filter(key => key !== groupState.key); |
|
|
} |
|
|
updateSavedMapSettings(); |
|
|
}); |
|
|
const getLayerOptions = (layerKey, groupKey, layerName) => { |
|
|
return { |
|
|
groupKey, |
|
|
layerKey, |
|
|
groupName: groupKey, |
|
|
layerName: layerName || categories[layerKey] || layerKey, |
|
|
|
|
|
|
|
|
image: images[layerKey] ? "/static/maps/interactive/${images[layerKey]}.png" : undefined, |
|
|
|
|
|
}; |
|
|
}; |
|
|
const addLayer = (layer, layerKey, groupKey, layerName) => { |
|
|
layer.key = layerKey; |
|
|
const layerOptions = getLayerOptions(layerKey, groupKey, layerName); |
|
|
if (!layerOptions.layerHidden) { |
|
|
layer.addTo(map); |
|
|
} |
|
|
layerControl.addOverlay(layer, layerOptions.layerName, layerOptions); |
|
|
}; |
|
|
map.layerControl = layerControl; |
|
|
|
|
|
const overlay = L.imageOverlay(imageUrl, getBounds(svgBounds)); |
|
|
overlay.addTo(map); |
|
|
function checkMarkerBounds(position, markerBounds) { |
|
|
if (position.x < markerBounds.TL.x) markerBounds.TL.x = position.x; |
|
|
if (position.z > markerBounds.TL.z) markerBounds.TL.z = position.z; |
|
|
if (position.x > markerBounds.BR.x) markerBounds.BR.x = position.x; |
|
|
if (position.z < markerBounds.BR.z) markerBounds.BR.z = position.z; |
|
|
} |
|
|
function getBounds(bounds) { |
|
|
if (!bounds) { |
|
|
return undefined; |
|
|
} |
|
|
return L.latLngBounds([bounds[0][1], bounds[0][0]], [bounds[1][1], bounds[1][0]]); |
|
|
|
|
|
} |
|
|
function mouseHoverOutline(event) { |
|
|
const outline = event.target.options.outline; |
|
|
if (event.originalEvent.type === 'mouseover') { |
|
|
outline._path.classList.remove('not-shown'); |
|
|
} else if (!outline._path.classList.contains('force-show')) { |
|
|
outline._path.classList.add('not-shown'); |
|
|
} |
|
|
} |
|
|
function toggleForceOutline(event) { |
|
|
const outline = event.target.options.outline; |
|
|
outline._path.classList.toggle('force-show'); |
|
|
if (outline._path.classList.contains('force-show')) { |
|
|
outline._path.classList.remove('not-shown'); |
|
|
} |
|
|
activateMarkerLayer(event); |
|
|
} |
|
|
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; |
|
|
} |
|
|
} |
|
|
} |
|
|
function outlineToPoly(outline) { |
|
|
if (!outline) return []; |
|
|
return outline.map(vector => [vector.z, vector.x]); |
|
|
} |
|
|
const layerOptions = { |
|
|
maxZoom: maxZoom, |
|
|
maxNativeZoom: maxZoom, |
|
|
extents: [ |
|
|
{ |
|
|
height: [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER], |
|
|
bounds: [bounds], |
|
|
} |
|
|
], |
|
|
type: 'map-layer', |
|
|
}; |
|
|
let tileLayer = false; |
|
|
const baseLayers = []; |
|
|
const tileSize = 256; |
|
|
let svgLayer = false; |
|
|
if (svgPath) { |
|
|
const svgBounds2 = svgBounds ? getBounds(svgBounds) : bounds; |
|
|
svgLayer = L.imageOverlay(svgPath, svgBounds2, layerOptions); |
|
|
baseLayers.push(svgLayer); |
|
|
} |
|
|
const positionIsInBounds = (position) => { |
|
|
return getBounds(bounds).contains(pos(position)); |
|
|
}; |
|
|
let markerBounds = { |
|
|
'TL': { x: Number.MAX_SAFE_INTEGER, z: Number.MIN_SAFE_INTEGER }, |
|
|
'BR': { x: Number.MIN_SAFE_INTEGER, z: Number.MAX_SAFE_INTEGER } |
|
|
} |
|
|
if (mapData.labels?.length > 0) { |
|
|
const labelsGroup = L.layerGroup(); |
|
|
const defaultHeight = ((layerOptions.extents[0].height[1] - layerOptions.extents[0].height[0]) / 2) + layerOptions.extents[0].height[0]; |
|
|
for (const label of mapData.labels) { |
|
|
const fontSize = label.size ? label.size : 100; |
|
|
const height = label.position.length < 3 ? defaultHeight : label.position[2]; |
|
|
const rotation = label.rotation ? label.rotation : 0; |
|
|
L.marker(pos({ x: label.position[0], z: label.position[1] }), { |
|
|
icon: L.divIcon({ html: `<div class="label" style="font-size: ${fontSize}%; transform: translate3d(-50%, -50%, 0) rotate(${rotation}deg)">${label.text}</div>`, className: 'map-area-label', layers: baseLayers, }), |
|
|
interactive: false, |
|
|
zIndexOffset: -100000, |
|
|
position: { |
|
|
x: label.position[0], |
|
|
y: height, |
|
|
z: label.position[1], |
|
|
}, |
|
|
top: typeof label.top !== 'undefined' ? label.top : 1000, |
|
|
bottom: typeof label.bottom !== 'undefined' ? label.bottom : -1000, |
|
|
}).addTo(labelsGroup); |
|
|
} |
|
|
addLayer(labelsGroup, 'place-names', 'Landmarks'); |
|
|
|
|
|
} |
|
|
|
|
|
if (mapData.spawns.length > 0) { |
|
|
const spawnLayers = { |
|
|
'pmc': L.layerGroup(), |
|
|
'scav': L.layerGroup(), |
|
|
'sniper_scav': L.layerGroup(), |
|
|
'boss': L.layerGroup(), |
|
|
'cultist-priest': L.layerGroup(), |
|
|
'rogue': L.layerGroup(), |
|
|
'bloodhound': L.layerGroup(), |
|
|
} |
|
|
for (const spawn of mapData.spawns) { |
|
|
if (!positionIsInBounds(spawn.position)) { |
|
|
continue; |
|
|
} |
|
|
let spawnType = ''; |
|
|
let bosses = []; |
|
|
if (spawn.categories.includes('boss')) { |
|
|
bosses = mapData.bosses.filter(boss => boss.spawnLocations.some(sl => sl.spawnKey === spawn.zoneName)); |
|
|
if (bosses.length === 0) { |
|
|
if (spawn.categories.includes('bot') && spawn.sides.includes('scav')) { |
|
|
spawnType = 'scav'; |
|
|
} |
|
|
else { |
|
|
console.error(`Unusual spawn: ${spawn.sides}, ${spawn.categories}`); |
|
|
continue; |
|
|
} |
|
|
} |
|
|
else if (bosses.length === 1 && (bosses[0].normalizedName === 'bloodhound' || bosses[0].normalizedName === 'cultist-priest' || bosses[0].normalizedName === 'rogue')) { |
|
|
spawnType = bosses[0].normalizedName; |
|
|
} |
|
|
else { |
|
|
spawnType = 'boss'; |
|
|
} |
|
|
} else if (spawn.categories.includes('sniper')) { |
|
|
spawnType = 'sniper_scav'; |
|
|
} else if (spawn.sides.includes('scav')) { |
|
|
if (spawn.categories.includes('bot') || spawn.categories.includes('all')) { |
|
|
spawnType = 'scav'; |
|
|
} |
|
|
else { |
|
|
console.error(`Unusual spawn: ${spawn.sides}, ${spawn.categories}`); |
|
|
continue; |
|
|
} |
|
|
} |
|
|
else if (spawn.categories.includes('player')) { |
|
|
if (spawn.sides.includes('pmc') || spawn.sides.includes('all')) { |
|
|
spawnType = 'pmc' |
|
|
} |
|
|
else { |
|
|
console.error(`Unusual spawn: ${spawn.sides}, ${spawn.categories}`); |
|
|
continue; |
|
|
} |
|
|
} |
|
|
else { |
|
|
console.error(`Unusual spawn: ${spawn.sides}, ${spawn.categories}`); |
|
|
continue; |
|
|
} |
|
|
const spawnIcon = L.icon({ |
|
|
iconUrl: `/static/maps/interactive/spawn_${spawnType}.png`, |
|
|
iconSize: [24, 24], |
|
|
popupAnchor: [0, -12], |
|
|
}); |
|
|
if (spawnType === 'pmc') { |
|
|
spawnIcon.iconAnchor = [12, 24]; |
|
|
spawnIcon.popupAnchor = [0, -24]; |
|
|
} |
|
|
const popupContent = L.DomUtil.create('div') |
|
|
if (spawn.categories.includes('boss') && bosses.length > 0) { |
|
|
bosses = bosses.reduce((unique, current) => { |
|
|
if (!unique.some(b => b.normalizedName === current.normalizedName)) { |
|
|
unique.push(current) |
|
|
if (!categories[`spawn_${current.normalizedName}`]) { |
|
|
categories[`spawn_${current.normalizedName}`] = current.name |
|
|
} |
|
|
} |
|
|
return unique; |
|
|
}, []); |
|
|
const bossList = L.DomUtil.create('div', undefined, popupContent) |
|
|
for (const boss of bosses) { |
|
|
if (bossList.childNodes.length > 0) { |
|
|
const comma = L.DomUtil.create('span', undefined, bossList) |
|
|
comma.textContent = ', ' |
|
|
} |
|
|
bossList.append(getALink(`https://escapefromtarkov.fandom.com/wiki/Special:Search?scope=internal&query=${boss.name}`, `${boss.name} (${Math.round(boss.spawnChance * 100)}%)`)) |
|
|
} |
|
|
} |
|
|
else { |
|
|
const spawnDiv = L.DomUtil.create('div', undefined, popupContent) |
|
|
spawnDiv.textContent = categories[`spawn_${spawnType}`] |
|
|
} |
|
|
addElevation(spawn, popupContent) |
|
|
const marker = L.marker(pos(spawn.position), { |
|
|
icon: spawnIcon, |
|
|
position: spawn.position, |
|
|
}); |
|
|
if (popupContent.childNodes.length > 0) { |
|
|
marker.bindPopup(L.popup().setContent(popupContent)) |
|
|
} |
|
|
marker.position = spawn.position |
|
|
marker.on('add', checkMarkerForActiveLayers) |
|
|
marker.on('click', activateMarkerLayer) |
|
|
marker.addTo(spawnLayers[spawnType]) |
|
|
checkMarkerBounds(spawn.position, markerBounds) |
|
|
} |
|
|
for (const key in spawnLayers) { |
|
|
if (Object.keys(spawnLayers[key]._layers).length > 0) { |
|
|
addLayer(spawnLayers[key], `spawn_${key}`, 'Spawns') |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
if (mapData.extracts.length > 0) { |
|
|
const extractLayers = { |
|
|
pmc: L.layerGroup(), |
|
|
scav: L.layerGroup(), |
|
|
shared: L.layerGroup(), |
|
|
} |
|
|
const zIndexOffsets = { |
|
|
pmc: 150, |
|
|
shared: 125, |
|
|
scav: 100, |
|
|
}; |
|
|
for (const extract of mapData.extracts) { |
|
|
const faction = extract.faction ?? 'shared'; |
|
|
if (!positionIsInBounds(extract.position)) { |
|
|
|
|
|
} |
|
|
const colorMap = { |
|
|
scav: '#ff7800', |
|
|
pmc: '#00e599', |
|
|
shared: '#00e4e5', |
|
|
} |
|
|
const rect = L.polygon(outlineToPoly(extract.outline), { color: colorMap[faction], weight: 1, className: 'not-shown' }); |
|
|
const extractIcon = L.divIcon({ |
|
|
className: 'extract-icon', |
|
|
html: `<img src="/static/maps/interactive/extract_${faction}.png"/><span class="extract-name ${faction}">${extract.name}</span>`, |
|
|
iconAnchor: [12, 12] |
|
|
}); |
|
|
const extractMarker = L.marker(pos(extract.position), { |
|
|
icon: extractIcon, |
|
|
title: extract.name, |
|
|
zIndexOffset: zIndexOffsets[faction], |
|
|
position: extract.position, |
|
|
top: extract.top, |
|
|
bottom: extract.bottom, |
|
|
outline: rect, |
|
|
id: extract.id, |
|
|
}); |
|
|
extractMarker.on('mouseover', mouseHoverOutline); |
|
|
extractMarker.on('mouseout', mouseHoverOutline); |
|
|
extractMarker.on('click', toggleForceOutline); |
|
|
if (extract.switches?.length > 0) { |
|
|
const popup = L.DomUtil.create('div'); |
|
|
const textElement = L.DomUtil.create('div'); |
|
|
textElement.textContent = `${tMaps('Activated by')}:`; |
|
|
popup.appendChild(textElement); |
|
|
for (const sw of extract.switches) { |
|
|
const linkElement = getPoiLinkElement(sw.id, 'switch'); |
|
|
const nameElement = L.DomUtil.create('span'); |
|
|
nameElement.innerHTML = `<strong>${sw.name}</strong>`; |
|
|
linkElement.append(nameElement); |
|
|
popup.appendChild(linkElement); |
|
|
} |
|
|
addElevation(extract, popup); |
|
|
extractMarker.bindPopup(L.popup().setContent(popup)); |
|
|
} else if (showElevation) { |
|
|
const popup = L.DomUtil.create('div'); |
|
|
addElevation(extract, popup); |
|
|
extractMarker.bindPopup(L.popup().setContent(popup)); |
|
|
} |
|
|
extractMarker.on('add', checkMarkerForActiveLayers); |
|
|
L.layerGroup([rect, extractMarker]).addTo(extractLayers[faction]); |
|
|
checkMarkerBounds(extract.position, markerBounds); |
|
|
} |
|
|
if (mapData.transits.length > 0) { |
|
|
extractLayers.transit = L.layerGroup(); |
|
|
for (const transit of mapData.transits) { |
|
|
if (!positionIsInBounds(transit.position)) { |
|
|
|
|
|
} |
|
|
const rect = L.polygon(outlineToPoly(transit.outline), { color: '#e53500', weight: 1, className: 'not-shown' }); |
|
|
const transitIcon = L.divIcon({ |
|
|
className: 'extract-icon', |
|
|
html: `<img src="/static/maps/interactive/extract_transit.png"/><span class="extract-name transit">${transit.description}</span>`, |
|
|
iconAnchor: [12, 12] |
|
|
}); |
|
|
const transitMarker = L.marker(pos(transit.position), { |
|
|
icon: transitIcon, |
|
|
title: transit.description, |
|
|
zIndexOffset: zIndexOffsets.pmc, |
|
|
position: transit.position, |
|
|
top: transit.top, |
|
|
bottom: transit.bottom, |
|
|
outline: rect, |
|
|
id: transit.id, |
|
|
}); |
|
|
transitMarker.on('mouseover', mouseHoverOutline); |
|
|
transitMarker.on('mouseout', mouseHoverOutline); |
|
|
transitMarker.on('click', toggleForceOutline); |
|
|
if (showElevation) { |
|
|
const popup = L.DomUtil.create('div'); |
|
|
addElevation(transit, popup); |
|
|
transitMarker.bindPopup(L.popup().setContent(popup)); |
|
|
} |
|
|
transitMarker.on('add', checkMarkerForActiveLayers); |
|
|
L.layerGroup([rect, transitMarker]).addTo(extractLayers.transit); |
|
|
checkMarkerBounds(transit.position, markerBounds); |
|
|
} |
|
|
} |
|
|
for (const key in extractLayers) { |
|
|
if (Object.keys(extractLayers[key]._layers).length > 0) { |
|
|
addLayer(extractLayers[key], `extract_${key}`, 'Extracts'); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
if (showStaticMarkers) { |
|
|
for (const category in mapData) { |
|
|
const markerLayer = L.layerGroup(); |
|
|
const items = mapData[category]; |
|
|
for (const item of items) { |
|
|
const itemIcon = L.icon({ |
|
|
iconUrl: `/static/maps/interactive/${category}.png`, |
|
|
iconSize: [24, 24], |
|
|
popupAnchor: [0, -12], |
|
|
|
|
|
}); |
|
|
L.marker(pos(item.position), { icon: itemIcon, position: item.position }) |
|
|
.bindPopup(L.popup().setContent(`${item.name}<br>Elevation: ${item.position.y}`)) |
|
|
.addTo(markerLayer); |
|
|
checkMarkerBounds(item.position, markerBounds); |
|
|
} |
|
|
if (items.length > 0) { |
|
|
var section; |
|
|
if (category.startsWith('extract')) { |
|
|
section = 'Extracts'; |
|
|
} |
|
|
else if (category.startsWith('spawn')) { |
|
|
section = 'Spawns'; |
|
|
} |
|
|
else { |
|
|
section = 'Lootable Items'; |
|
|
} |
|
|
markerLayer.addTo(map); |
|
|
addLayer(markerLayer, category, section); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
const customControl = L.Control.extend({ |
|
|
onAdd: function (map) { |
|
|
this.container = L.DomUtil.create('div', 'custom-control leaflet-control-layers leaflet-control-layers-expanded') |
|
|
this.container.innerHTML = '<h2>Add Marker to Pos: </h2>' |
|
|
return this.container |
|
|
}, |
|
|
updateText: function (html) { |
|
|
if (this.container) { |
|
|
this.container.innerHTML = html |
|
|
} |
|
|
}, |
|
|
clearText: function () { |
|
|
if (this.container) { |
|
|
this.container.innerHTML = '' |
|
|
} |
|
|
}, |
|
|
onRemove: function (map) { |
|
|
|
|
|
} |
|
|
}); |
|
|
const myControl = new customControl({ position: 'topright' }) |
|
|
map.addControl(myControl) |
|
|
function startCountdown(elementId) { |
|
|
const element = document.getElementById(elementId); |
|
|
if (!element) { |
|
|
console.error(`Element with id ${elementId} not found.`); |
|
|
return; |
|
|
} |
|
|
|
|
|
let secondsStart = parseInt(element.getAttribute('data-seconds')) - 60 |
|
|
let seconds = secondsStart |
|
|
if (isNaN(secondsStart)) { |
|
|
console.error('Invalid data-seconds attribute.'); |
|
|
return; |
|
|
} |
|
|
const intervalId = setInterval(() => { |
|
|
if (!document.hidden) { |
|
|
|
|
|
|
|
|
seconds-- |
|
|
if (seconds <= 0) { |
|
|
clearInterval(intervalId); |
|
|
element.textContent = '0:00:00'; |
|
|
myControl.updateText("") |
|
|
document.querySelector("head > title").textContent = `${mapData.name} | Map with friends` |
|
|
return; |
|
|
} |
|
|
|
|
|
const hours = Math.floor(seconds / 3600); |
|
|
const minutes = Math.floor((seconds % 3600) / 60); |
|
|
const remainingSeconds = seconds % 60; |
|
|
const timeString = `${hours}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`; |
|
|
element.textContent = timeString; |
|
|
document.querySelector("head > title").textContent = `${mapData.name} | ${timeString}` |
|
|
} |
|
|
}, 1000); |
|
|
} |
|
|
function addNewRadeData(new_html) { |
|
|
myControl.updateText(new_html) |
|
|
startCountdown("rade_time_remain") |
|
|
}; |
|
|
|
|
|
|
|
|
function addMarker(x, y, z, timestamp, preview, actualmap, playername, markercolor) { |
|
|
|
|
|
const position = { |
|
|
x: parseFloat(x), |
|
|
y: parseFloat(y), |
|
|
z: parseFloat(z) |
|
|
}; |
|
|
if (!positionIsInBounds(position)) { |
|
|
console.error("Position außerhalb der Karte:", position); |
|
|
return; |
|
|
} |
|
|
|
|
|
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> |
|
|
`; |
|
|
const svgString2 = ` |
|
|
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 384 512' class="marker-svg"> |
|
|
<path fill='${markerColor}' d='M172.3 501.7C27 291 0 269.4 0 192 0 86 86 0 192 0s192 86 192 192c0 77.4-27 99-172.3 309.7-9.5 13.8-29.9 13.8-39.5 0zM192 272c44.2 0 80-35.8 80-80s-35.8-80-80-80-80 35.8-80 80 35.8 80 80 80z'/> |
|
|
</svg> |
|
|
`; |
|
|
|
|
|
const markerIcon = L.divIcon({ |
|
|
html: svgString, |
|
|
className: 'custom-marker', |
|
|
iconSize: [32, 32], |
|
|
iconAnchor: [16, 32], |
|
|
popupAnchor: [0, -32] |
|
|
}); |
|
|
const playerMarkerName = playername.toLowerCase().replaceAll(" ", "-") |
|
|
|
|
|
if (playerMarkers[playerMarkerName]) { |
|
|
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}"><br>` : ''} |
|
|
<small>${timestamp}</small> |
|
|
</div> |
|
|
`; |
|
|
playerMarkers[playerMarkerName].bindPopup(popupContent, { |
|
|
maxWidth: 250, |
|
|
minWidth: 150, |
|
|
autoClose: true, |
|
|
closeOnClick: true |
|
|
}); |
|
|
|
|
|
playerMarkers[playerMarkerName].addTo(map); |
|
|
|
|
|
map.setView(pos(position), map.getZoom(), { |
|
|
animate: true, |
|
|
duration: 0.5 |
|
|
}); |
|
|
console.log("Neuer Marker gesetzt für " + playername + ": " + position); |
|
|
} |
|
|
document.onload = loadLocalData() |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
|
|
|
function openModal($el) { |
|
|
$el.classList.add('is-active'); |
|
|
} |
|
|
function closeModal($el) { |
|
|
$el.classList.remove('is-active'); |
|
|
} |
|
|
function closeAllModals() { |
|
|
(document.querySelectorAll('.modal') || []).forEach(($modal) => { |
|
|
closeModal($modal); |
|
|
}); |
|
|
} |
|
|
|
|
|
(document.querySelectorAll('.js-modal-trigger') || []).forEach(($trigger) => { |
|
|
const modal = $trigger.dataset.target; |
|
|
const $target = document.getElementById(modal); |
|
|
$trigger.addEventListener('click', () => { |
|
|
openModal($target); |
|
|
}); |
|
|
}); |
|
|
|
|
|
(document.querySelectorAll('.modal-background, .modal-close, .modal-card-head .delete, .modal-card-foot .button') || []).forEach(($close) => { |
|
|
const $target = $close.closest('.modal'); |
|
|
$close.addEventListener('click', () => { |
|
|
closeModal($target); |
|
|
}); |
|
|
}); |
|
|
|
|
|
document.addEventListener('keydown', (event) => { |
|
|
if (event.key === "Escape") { |
|
|
closeAllModals(); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
</script> |
|
|
</body> |