Download templates/map.html from Sebastiankay/eft_group_map_websocket: direct link, hf CLI and curl.
- Browser
- Download file 41.8 kB
-
https://huggingface.co/spaces/Sebastiankay/eft_group_map_websocket/resolve/main/templates/map.html
- Command line
-
hf download hf://spaces/Sebastiankay/eft_group_map_websocket/templates/map.html
-
curl -L -o map.html https://huggingface.co/spaces/Sebastiankay/eft_group_map_websocket/resolve/main/templates/map.html
41.8 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <!--title>Tarkov Map with friends</title--> | |
| <title>{{ maps[0].name }} | Map with friends</title> | |
| <link rel="icon" type="image/x-icon" href="/static/favicon.ico" /> | |
| <!-- Leaflet --> | |
| <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> | |
| <!-- Tailwind --> | |
| <!--script src="https://cdn.tailwindcss.com"></script--> | |
| <!-- Bootstrap --> | |
| <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" rel="stylesheet"> | |
| <!-- Custom Styles --> | |
| <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> | |
| <!-- Your content --> | |
| <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> | |
| <!-- Scripts --> | |
| <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> | |
| // GLOBAL VARIABLE TO HOLD THE CURRENT MARKER | |
| const playerMarkers = {} | |
| let currentMarker = null | |
| const client_id = Date.now() | |
| const mapData = {{ maps[0] | tojson }} | |
| console.info(mapData) | |
| //const transform = [0.1855, 113.1, 0.1855, 167.8] | |
| 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; | |
| //MARK: INIT WEBSOCKET | |
| // Initialize WebSocket connection | |
| const ws = new WebSocket(`wss://sebastiankay-eft-group-map-websocket.hf.space/ws`); | |
| // WebSocket event handlers | |
| 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(); // Clear all markers when the map changes | |
| 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 to remove all markers | |
| 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 to load and display local marker data | |
| 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); | |
| // Use the new addMarker function with all required parameters | |
| 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; | |
| // check if marker is completely contained by inactive layer | |
| 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; | |
| } | |
| } | |
| } | |
| // check if marker is on active overlay | |
| const activeOverlay = Object.values(map._layers).find(l => l.options?.extents && l.options?.overlay); | |
| if (activeOverlay && markerIsOnLayer(marker, activeOverlay)) { | |
| return true; | |
| } | |
| // check if marker is on base layer | |
| 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); | |
| // a.addEventListener('click', (event) => { | |
| // navigate(path); | |
| // event.preventDefault(); | |
| // }); | |
| return a; | |
| }; | |
| function getScaledBounds(bounds, scaleFactor) { | |
| // Calculate the center point of the bounds | |
| const centerX = (bounds[0][0] + bounds[1][0]) / 2; | |
| const centerY = (bounds[0][1] + bounds[1][1]) / 2; | |
| // Calculate the new width and height | |
| const width = bounds[1][0] - bounds[0][0]; | |
| const height = bounds[1][1] - bounds[0][1]; | |
| const newWidth = width * scaleFactor; | |
| const newHeight = height * scaleFactor; | |
| // Update the coordinates of the two points defining the bounds | |
| const newBounds = [ | |
| [centerY - newHeight / 2, centerX - newWidth / 2], | |
| [centerY + newHeight / 2, centerX + newWidth / 2] | |
| ]; | |
| // console.log("Initial Rectangle:", bounds); | |
| // console.log("Scaled Rectangle:", newBounds); | |
| // console.log("Center:", L.bounds(bounds).getCenter(true)); | |
| return newBounds; | |
| } | |
| // Erstelle die Karte | |
| //const map = L.map('map').setView([0, 0], 2); | |
| const map = L.map('map', { | |
| maxBounds: getScaledBounds(svgBounds, 1.5), | |
| //maxBounds: bounds, | |
| 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, | |
| //groupHidden: Boolean(mapSettingsRef.current.hiddenGroups?.includes(groupKey)), | |
| //layerHidden: Boolean(mapSettingsRef.current.hiddenLayers?.includes(layerKey)), | |
| image: images[layerKey] ? "/static/maps/interactive/${images[layerKey]}.png" : undefined, | |
| //groupCollapsed: Boolean(mapSettingsRef.current.collapsedGroups?.includes(groupKey)), | |
| }; | |
| }; | |
| 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; | |
| // Hinzufügen des Image-Overlays | |
| 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]]); | |
| //return [[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'); | |
| //labelsGroup.addTo(map); | |
| } | |
| // Add spawns | |
| 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') | |
| } | |
| } | |
| } | |
| //add extracts | |
| 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)) { | |
| //continue; | |
| } | |
| 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)) { | |
| //continue; | |
| } | |
| 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'); | |
| } | |
| } | |
| } | |
| // Add static items | |
| 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], | |
| //className: layerIncludesMarker(heightLayer, item) ? '' : 'off-level', | |
| }); | |
| 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); | |
| // layerControl.addOverlay(markerLayer, `<img src='${process.env.PUBLIC_URL}/maps/interactive/${category}.png' class='control-item-image' /> ${categories[category] || category}`, section); | |
| } | |
| } | |
| } | |
| // Erstelle ein neues Control-Element | |
| 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) { | |
| // Nichts zu tun hier | |
| } | |
| }); | |
| 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 = Math.floor(new Date().getTime() / 1000.0) - parseInt(element.getAttribute('data-rade-end')) - 60 | |
| 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 = (secondsStart - parseInt(Math.floor(new Date().getTime() / 1000.0))) | |
| //localStorage.setItem('rade_data', JSON.stringify({ remaining_time: seconds })); | |
| seconds-- | |
| if (seconds <= 0) { | |
| clearInterval(intervalId); | |
| element.textContent = '0:00:00'; | |
| myControl.updateText("") | |
| document.querySelector("head > title").textContent = `${mapData.name} | Map with friends` | |
| return; | |
| } | |
| //seconds--; | |
| 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") | |
| }; | |
| //MARK: ADDMARKER | |
| // Funktion zum Hinzufügen eines Markers | |
| function addMarker(x, y, z, timestamp, preview, actualmap, playername, markercolor) { | |
| // Validate position | |
| const position = { | |
| x: parseFloat(x), | |
| y: parseFloat(y), | |
| z: parseFloat(z) | |
| }; | |
| if (!positionIsInBounds(position)) { | |
| console.error("Position außerhalb der Karte:", position); | |
| return; | |
| } | |
| // SVG-Icon als String mit dynamischer Farbe | |
| 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> | |
| `; | |
| // DivIcon mit dynamischem SVG erstellen | |
| const markerIcon = L.divIcon({ | |
| html: svgString, | |
| className: 'custom-marker', | |
| iconSize: [32, 32], | |
| iconAnchor: [16, 32], | |
| popupAnchor: [0, -32] | |
| }); | |
| const playerMarkerName = playername.toLowerCase().replaceAll(" ", "-") | |
| // Remove old marker for this player, if it exists | |
| if (playerMarkers[playerMarkerName]) { | |
| map.removeLayer(playerMarkers[playerMarkerName]); | |
| } | |
| // Create new marker | |
| playerMarkers[playerMarkerName] = L.marker(pos(position), { | |
| icon: markerIcon, | |
| position: position, | |
| title: `Koordinaten: ${x}, ${y}, ${z}`, | |
| riseOnHover: true, | |
| zIndexOffset: 400 | |
| }); | |
| // Popup mit Informationen erstellen | |
| 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 | |
| }); | |
| // Marker zur Karte hinzufügen | |
| playerMarkers[playerMarkerName].addTo(map); | |
| // Karte auf Marker zentrieren | |
| map.setView(pos(position), map.getZoom(), { | |
| animate: true, | |
| duration: 0.5 | |
| }); | |
| console.log("Neuer Marker gesetzt für " + playername + ": " + position); | |
| } | |
| document.onload = loadLocalData() | |
| // MODAL Trigger | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Functions to open and close a modal | |
| 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); | |
| }); | |
| } | |
| // Add a click event on buttons to open a specific modal | |
| (document.querySelectorAll('.js-modal-trigger') || []).forEach(($trigger) => { | |
| const modal = $trigger.dataset.target; | |
| const $target = document.getElementById(modal); | |
| $trigger.addEventListener('click', () => { | |
| openModal($target); | |
| }); | |
| }); | |
| // Add a click event on various child elements to close the parent modal | |
| (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); | |
| }); | |
| }); | |
| // Add a keyboard event to close all modals | |
| document.addEventListener('keydown', (event) => { | |
| if (event.key === "Escape") { | |
| closeAllModals(); | |
| } | |
| }); | |
| }); | |
| </script> | |
| </body> |