Sebastiankay/eft_group_map_websocket
0
1<!DOCTYPE html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 <title>{{ maps[0].name }} | Map with friends</title>
7 <link rel="icon" type="image/x-icon" href="/static/favicon.ico" />
8 <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css" />
9 <!--link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet-groupedlayercontrol/0.6.1/leaflet.groupedlayercontrol.css"/-->
10 <script src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"></script>
11 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" />
12 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css" />
13 <link rel="stylesheet" href="/static/style.css" />
14 </head>
15
16 <body>
17 <nav class="navbar is-fixed-top" role="navigation" aria-label="main navigation">
18 <div class="navbar-brand">
19 <a class="navbar-item" href="https://bulma.io">
20 <svg width="640" height="160" viewBox="0 0 640 160" fill="none" xmlns="http://www.w3.org/2000/svg">
21 <path
22 fill-rule="evenodd"
23 clip-rule="evenodd"
24 d="M170 132.571V27.5908C170 25.5451 170.915 23.93 172.746 22.7456C174.576 21.5612 176.729 20.969 179.206 20.969H210.377C232.019 20.969 242.84 30.4441 242.84 49.3943C242.84 62.5303 238.264 71.0902 229.112 75.074C234.603 77.2275 238.748 80.2692 241.548 84.1992C244.347 88.1292 245.747 93.8627 245.747 101.4V104.791C245.747 116.743 242.84 125.437 237.026 130.875C231.211 136.312 223.351 139.031 213.445 139.031H179.206C176.514 139.031 174.307 138.385 172.584 137.093C170.861 135.801 170 134.293 170 132.571ZM190.834 120.619H209.085C219.529 120.619 224.751 114.751 224.751 103.015V100.431C224.751 94.401 223.432 90.0404 220.794 87.3486C218.156 84.6568 214.253 83.3109 209.085 83.3109H190.834V120.619ZM190.834 66.8371H208.923C213.122 66.8371 216.326 65.5989 218.533 63.1225C220.74 60.646 221.844 57.2544 221.844 52.9475C221.844 48.7483 220.686 45.4374 218.371 43.0148C216.057 40.5922 212.853 39.3809 208.762 39.3809H190.834V66.8371ZM260.283 103.015V27.4293C260.283 25.2759 261.306 23.6608 263.351 22.5841C265.397 21.5074 267.873 20.969 270.781 20.969C273.688 20.969 276.164 21.5074 278.21 22.5841C280.256 23.6608 281.279 25.2759 281.279 27.4293V103.015C281.279 115.397 287.2 121.588 299.044 121.588C310.888 121.588 316.81 115.397 316.81 103.015V27.4293C316.81 25.2759 317.833 23.6608 319.879 22.5841C321.925 21.5074 324.401 20.969 327.308 20.969C330.215 20.969 332.692 21.5074 334.738 22.5841C336.783 23.6608 337.806 25.2759 337.806 27.4293V103.015C337.806 115.72 334.28 125.061 327.227 131.036C320.175 137.012 310.781 140 299.044 140C287.308 140 277.914 137.039 270.861 131.117C263.809 125.195 260.283 115.828 260.283 103.015ZM356.703 132.409V27.4293C356.703 25.2759 357.725 23.6608 359.771 22.5841C361.817 21.5074 364.293 20.969 367.201 20.969C370.108 20.969 372.584 21.5074 374.63 22.5841C376.676 23.6608 377.699 25.2759 377.699 27.4293V120.619H417.106C419.044 120.619 420.579 121.534 421.709 123.365C422.84 125.195 423.405 127.349 423.405 129.825C423.405 132.301 422.84 134.455 421.709 136.285C420.579 138.116 419.044 139.031 417.106 139.031H365.908C363.432 139.031 361.279 138.439 359.448 137.254C357.618 136.07 356.703 134.455 356.703 132.409ZM434.872 132.409V31.467C434.872 27.9138 435.868 25.2759 437.86 23.5532C439.852 21.8304 442.355 20.969 445.37 20.969C449.354 20.969 452.423 21.6689 454.576 23.0686C456.729 24.4684 459.098 27.4832 461.682 32.1131L481.548 68.2907L501.413 32.1131C503.997 27.4832 506.393 24.4684 508.6 23.0686C510.808 21.6689 513.903 20.969 517.887 20.969C520.902 20.969 523.405 21.8304 525.397 23.5532C527.389 25.2759 528.385 27.9138 528.385 31.467V132.409C528.385 134.455 527.335 136.07 525.236 137.254C523.136 138.439 520.686 139.031 517.887 139.031C514.98 139.031 512.503 138.439 510.458 137.254C508.412 136.07 507.389 134.455 507.389 132.409V62.961L488.493 96.5545C486.985 99.354 484.616 100.754 481.386 100.754C478.264 100.754 475.949 99.354 474.441 96.5545L455.868 61.6689V132.409C455.868 134.455 454.818 136.07 452.719 137.254C450.619 138.439 448.17 139.031 445.37 139.031C442.463 139.031 439.987 138.439 437.941 137.254C435.895 136.07 434.872 134.455 434.872 132.409ZM539.529 130.31C539.529 130.094 539.637 129.556 539.852 128.694L571.023 27.1063C571.669 24.8452 573.257 23.0956 575.787 21.8573C578.318 20.6191 581.198 20 584.428 20C587.658 20 590.565 20.6191 593.149 21.8573C595.734 23.0956 597.349 24.8452 597.995 27.1063L629.166 128.694C629.381 129.556 629.489 130.094 629.489 130.31C629.489 132.678 628.035 134.724 625.128 136.447C622.221 138.17 619.26 139.031 616.245 139.031C612.261 139.031 609.892 137.631 609.139 134.832L603.001 113.351H566.016L559.879 134.832C559.125 137.631 556.756 139.031 552.773 139.031C549.65 139.031 546.662 138.197 543.809 136.528C540.956 134.859 539.529 132.786 539.529 130.31ZM570.377 96.8775H598.479L584.428 47.2948L570.377 96.8775Z"
25 fill="black"
26 class="bd-svg-black"
27 />
28 <path fill-rule="evenodd" clip-rule="evenodd" d="M0 110L10 40L50 0L100 50L70 80L110 120L50 160L0 110Z" fill="#00D1B2" />
29 </svg>
30 </a>
31
32 <a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
33 <span aria-hidden="true"></span>
34 <span aria-hidden="true"></span>
35 <span aria-hidden="true"></span>
36 <span aria-hidden="true"></span>
37 </a>
38 </div>
39
40 <div id="navbarBasicExample" class="navbar-menu">
41 <div class="navbar-start">
42 <a class="navbar-item"> Home </a>
43 <div class="navbar-item has-dropdown is-hoverable">
44 <a class="navbar-link">Maps</a>
45 <div class="navbar-dropdown is-boxed">
46 <a href="/map/woods" class="navbar-item">Woods</a>
47 <a href="/map/shoreline" class="navbar-item">Shoreline</a>
48 <a href="/map/lighthouse" class="navbar-item">Lighthouse</a>
49 <a href="/map/customs" class="navbar-item">Customs</a>
50 <a href="/map/interchange" class="navbar-item">Interchange</a>
51 <a href="/map/streets-of-tarkov" class="navbar-item">Streets of Tarkov</a>
52 <a href="/map/ground-zero" class="navbar-item">Ground Zero</a>
53 <a href="/map/reserve" class="navbar-item">Reserve</a>
54 <a href="/map/factory" class="navbar-item">Factory</a>
55 </div>
56 </div>
57 </div>
58
59 <div class="navbar-end">
60 <div class="navbar-item">
61 <button class="button is-dark js-modal-trigger" data-target="settings-modal">
62 <span class="icon is-small">
63 <i class="fas fa-cog"></i>
64 </span>
65 </button>
66 </div>
67 </div>
68 </div>
69 </nav>
70
71 <div id="map"></div>
72
73 <div id="settings-modal" class="modal">
74 <div class="modal-background"></div>
75
76 <div class="modal-content container">
77 <div class="box">
78 <section class="hero">
79 <div class="hero-body">
80 <p class="title has-text-weight-bold">EINSTELLUNGEN</p>
81 <hr class="title-line" />
82 <p class="subtitle">Subtitle</p>
83 </div>
84 </section>
85 <!-- Your content -->
86 <section class="section">
87 <div class="content">
88 <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>
89 </div>
90 </section>
91 </div>
92 </div>
93
94 <button class="modal-close is-large" aria-label="close"></button>
95 </div>
96
97 <script src="/static/js/leaflet.controlGroupedlayer.js"></script>
98 <script src="/static/js/leaflet.controlCoordinates.js"></script>
99 <script>
100 // GLOBAL VARIABLE TO HOLD THE CURRENT MARKER
101 let currentMarker = null;
102 const client_id = Date.now()
103 const mapData = {{ maps[0] | tojson }}
104 console.info(mapData)
105 //const transform = [0.1855, 113.1, 0.1855, 167.8]
106 const transform = mapData.transform
107 const bounds = mapData.bounds
108 const svgBounds = mapData.svgBounds ? mapData.svgBounds : bounds
109 const coordinateRotation = mapData.coordinateRotation ? mapData.coordinateRotation : 0
110 const svgPath = mapData.svgPath
111 const imageUrl = mapData.svgPath
112 const minZoom = mapData.minZoom ? mapData.minZoom : 1
113 const maxZoom = mapData.maxZoom ? mapData.maxZoom : 6
114 const showElevation = false;
115 const showStaticMarkers = false
116
117 let currentZoom = 3;
118
119
120 const ws = new WebSocket(`ws://localhost:8000/${client_id}`)
121 ws.onmessage = function (event) {
122 console.log(event.data);
123 const data = JSON.parse(event.data);
124 if (data.type === "coordinates") {
125 const parsedData = data.data;
126 localStorage.setItem("last_marker", JSON.stringify(parsedData));
127 loadLocalData();
128 }
129 if (data.type === "location_map") {
130 const map_name = data.data.map.toLowerCase().replaceAll(" ", "-")
131 localStorage.removeItem("last_marker")
132 if (!location.pathname.includes(map_name)) location.pathname = `/map/${map_name}`
133 else location.reload()
134 }
135 };
136
137 function loadLocalData() {
138
139 const markerData = JSON.parse(localStorage.getItem("last_marker"))
140 console.log(markerData)
141 if (markerData) {
142 // Alten Marker entfernen
143 if (currentMarker) {
144 map.removeLayer(currentMarker);
145 currentMarker = null;
146 }
147 // Neuen Marker hinzufügen
148 addMarker(markerData.x, markerData.y, markerData.z, markerData.timestamp, markerData.preview);
149 }
150 };
151
152 const images = {
153 'container_bank-cash-register': 'container_cash-register',
154 'container_bank-safe': 'container_safe',
155 'container_buried-barrel-cache': 'container_buried-barrel-cache',
156 'container_cash-register': 'container_cash-register',
157 'container_cash-register-tar2-2': 'container_cash-register',
158 'container_dead-civilian': 'container_dead-scav',
159 'container_dead-scav': 'container_dead-scav',
160 'container_festive-airdrop-supply-crate': 'container_festive-airdrop-supply-crate',
161 'container_pmc-body': 'container_dead-scav',
162 'container_civilian-body': 'container_dead-scav',
163 'container_drawer': 'container_drawer',
164 'container_duffle-bag': 'container_duffle-bag',
165 'container_grenade-box': 'container_grenade-box',
166 'container_ground-cache': 'container_ground-cache',
167 'container_jacket': 'container_jacket',
168 'container_lab-technician-body': 'container_dead-scav',
169 'container_medbag-smu06': 'container_medbag-smu06',
170 'container_medcase': 'container_medcase',
171 'container_medical-supply-crate': 'container_crate',
172 'container_pc-block': 'container_pc-block',
173 'container_plastic-suitcase': 'container_plastic-suitcase',
174 'container_ration-supply-crate': 'container_crate',
175 'container_safe': 'container_safe',
176 'container_scav-body': 'container_dead-scav',
177 'container_shturmans-stash': 'container_weapon-box',
178 'container_technical-supply-crate': 'container_crate',
179 'container_toolbox': 'container_toolbox',
180 'container_weapon-box': 'container_weapon-box',
181 'container_wooden-ammo-box': 'container_wooden-ammo-box',
182 'container_wooden-crate': 'container_wooden-crate',
183 'extract_pmc': 'extract_pmc',
184 'extract_scav': 'extract_scav',
185 'extract_shared': 'extract_shared',
186 'extract_transit': 'extract_transit',
187 'hazard': 'hazard',
188 'hazard_mortar': 'hazard_mortar',
189 'hazard_minefield': 'hazard',
190 'hazard_sniper': 'hazard',
191 'key': 'key',
192 'lock': 'lock',
193 'loose_loot': 'loose_loot',
194 'quest_item': 'quest_item',
195 'quest_objective': 'quest_objective',
196 'spawn_sniper_scav': 'spawn_sniper_scav',
197 'spawn_bloodhound': 'spawn_bloodhound',
198 'spawn_boss': 'spawn_boss',
199 'spawn_cultist-priest': 'spawn_cultist-priest',
200 'spawn_pmc': 'spawn_pmc',
201 'spawn_rogue': 'spawn_rogue',
202 'spawn_scav': 'spawn_scav',
203 'stationarygun': 'stationarygun',
204 'switch': 'switch',
205 };
206
207 const categories = {
208 'extract_pmc': 'PMC',
209 'extract_shared': 'Shared',
210 'extract_scav': 'Scav',
211 'extract_transit': 'Transit',
212 'spawn_sniper_scav': 'Sniper Scav',
213 'spawn_pmc': 'PMC',
214 'spawn_scav': 'Scav',
215 'spawn_boss': 'Boss',
216 'quest_item': 'Item',
217 'quest_objective': 'Objective',
218 'lock': 'Locks',
219 'lever': 'Lever',
220 'stationarygun': 'Stationary Gun',
221 'switch': 'Switch',
222 'place-names': 'Place Names',
223 };
224
225 function getCRS(transform) {
226 let scaleX = 1;
227 let scaleY = 1;
228 let marginX = 0;
229 let marginY = 0;
230 if (transform) {
231 scaleX = transform[0];
232 scaleY = transform[2] * -1;
233 marginX = transform[1];
234 marginY = transform[3];
235 }
236 return L.extend({}, L.CRS.Simple, {
237 transformation: new L.Transformation(scaleX, marginX, scaleY, marginY),
238 projection: L.extend({}, L.Projection.LonLat, {
239 project: latLng => {
240 return L.Projection.LonLat.project(applyRotation(latLng, coordinateRotation));
241 },
242 unproject: point => {
243 return applyRotation(L.Projection.LonLat.unproject(point), coordinateRotation * -1);
244 },
245 }),
246 });
247 }
248
249 function applyRotation(latLng, rotation) {
250 if (!latLng.lng && !latLng.lat) {
251 return L.latLng(0, 0);
252 }
253 if (!rotation) {
254 return latLng;
255 }
256
257 const angleInRadians = (rotation * Math.PI) / 180;
258 const cosAngle = Math.cos(angleInRadians);
259 const sinAngle = Math.sin(angleInRadians);
260
261 const { lng: x, lat: y } = latLng;
262 const rotatedX = x * cosAngle - y * sinAngle;
263 const rotatedY = x * sinAngle + y * cosAngle;
264 return L.latLng(rotatedY, rotatedX);
265 }
266
267 function pos(position) {
268 return [position.z, position.x];
269 }
270
271 function addElevation(item, popup) {
272 if (!showElevation) {
273 return;
274 }
275 const elevationContent = L.DomUtil.create('div', undefined, popup);
276 elevationContent.textContent = `Elevation: ${item.position.y.toFixed(2)}`;
277 if (item.top && item.bottom && item.top !== item.position.y && item.bottom !== item.position.y) {
278 const heightContent = L.DomUtil.create('div', undefined, popup);
279 heightContent.textContent = `Top ${item.top.toFixed(2)}, bottom: ${item.bottom.toFixed(2)}`;
280 }
281 }
282
283 function markerIsOnLayer(marker, layer) {
284 if (!layer) {
285 return true;
286 }
287 var top = marker.options.top || marker.options.position.y;
288 var bottom = marker.options.bottom || marker.options.position.y;
289 for (const extent of layer.options.extents) {
290 if (top >= extent.height[0] && bottom < extent.height[1]) {
291 let containedType = 'partial';
292 if (bottom >= extent.height[0] && top <= extent.height[1]) {
293 containedType = 'full';
294 }
295 if (extent.bounds) {
296 for (const boundsArray of extent.bounds) {
297 const bounds = getBounds(boundsArray);
298 if (bounds.contains(pos(marker.options.position))) {
299 return containedType;
300 }
301 }
302 } else {
303 return containedType;
304 }
305 }
306 }
307 return false;
308 }
309
310 function markerIsOnActiveLayer(marker) {
311 if (!marker.options.position) {
312 return true;
313 }
314
315 const map = marker._map;
316
317 // check if marker is completely contained by inactive layer
318 const overlays = map.layerControl._layers.map(l => l.layer).filter(l => Boolean(l.options.extents) && l.options.overlay);
319 for (const layer of overlays) {
320 for (const extent of layer.options.extents) {
321 if (markerIsOnLayer(marker, layer) === 'full' && !map.hasLayer(layer) && extent.bounds) {
322 return false;
323 }
324 }
325 }
326
327 // check if marker is on active overlay
328 const activeOverlay = Object.values(map._layers).find(l => l.options?.extents && l.options?.overlay);
329 if (activeOverlay && markerIsOnLayer(marker, activeOverlay)) {
330 return true;
331 }
332
333 // check if marker is on base layer
334 const baseLayer = Object.values(map._layers).find(l => l.options?.extents && !L.options?.overlay);
335 if (!activeOverlay && markerIsOnLayer(marker, baseLayer)) {
336 return true;
337 }
338
339 return false;
340 }
341
342 function checkMarkerForActiveLayers(event) {
343 const marker = event.target || event;
344 const outline = marker.options.outline;
345 const onLevel = markerIsOnActiveLayer(marker);
346 if (onLevel) {
347 marker._icon?.classList.remove('off-level');
348 if (outline) {
349 outline._path?.classList.remove('off-level');
350 }
351 } else {
352 marker._icon?.classList.add('off-level');
353 if (outline) {
354 outline._path?.classList.add('off-level');
355 }
356 }
357 }
358
359 function activateMarkerLayer(event) {
360 const marker = event.target || event;
361 if (markerIsOnActiveLayer(marker)) {
362 return;
363 }
364 const activeLayers = Object.values(marker._map._layers).filter(l => l.options?.extents && l.options?.overlay);
365 for (const layer of activeLayers) {
366 layer.removeFrom(marker._map);
367 }
368 const heightLayers = marker._map.layerControl._layers.filter(l => l.layer.options.extents && l.layer.options.overlay).map(l => l.layer);
369 for (const layer of heightLayers) {
370 if (markerIsOnLayer(marker, layer)) {
371 layer.addTo(marker._map);
372 break;
373 }
374 }
375 }
376
377 const getALink = (path, contents) => {
378 const a = L.DomUtil.create('a');
379 a.setAttribute('href', path);
380 a.setAttribute('target', '_blank');
381 a.append(contents);
382 // a.addEventListener('click', (event) => {
383 // navigate(path);
384 // event.preventDefault();
385 // });
386 return a;
387 };
388
389 function getScaledBounds(bounds, scaleFactor) {
390 // Calculate the center point of the bounds
391 const centerX = (bounds[0][0] + bounds[1][0]) / 2;
392 const centerY = (bounds[0][1] + bounds[1][1]) / 2;
393
394 // Calculate the new width and height
395 const width = bounds[1][0] - bounds[0][0];
396 const height = bounds[1][1] - bounds[0][1];
397 const newWidth = width * scaleFactor;
398 const newHeight = height * scaleFactor;
399
400 // Update the coordinates of the two points defining the bounds
401 const newBounds = [
402 [centerY - newHeight / 2, centerX - newWidth / 2],
403 [centerY + newHeight / 2, centerX + newWidth / 2]
404 ];
405
406 // console.log("Initial Rectangle:", bounds);
407 // console.log("Scaled Rectangle:", newBounds);
408 // console.log("Center:", L.bounds(bounds).getCenter(true));
409
410 return newBounds;
411 }
412
413 // Erstelle die Karte
414 //const map = L.map('map').setView([0, 0], 2);
415
416 const map = L.map('map', {
417 maxBounds: getScaledBounds(svgBounds, 1.5),
418 //maxBounds: bounds,
419 center: [0, 0],
420 zoom: 2,
421 minZoom: minZoom,
422 maxZoom: maxZoom,
423 zoomSnap: 0.1,
424 scrollWheelZoom: true,
425 wheelPxPerZoomLevel: 120,
426 crs: getCRS(transform),
427 attributionControl: false,
428 id: "wwoodsMap",
429 });
430
431 const layerControl = L.control.groupedLayers(null, null, {
432 position: 'topleft',
433 collapsed: true,
434 groupCheckboxes: true,
435 groupsCollapsable: true,
436 exclusiveOptionalGroups: ['Levels'],
437 }).addTo(map);
438 layerControl.on('overlayToggle', (e) => {
439 const layerState = e.detail;
440 if (layerState.checked) {
441 mapViewRef.current.layer = layerState.key;
442 } else {
443 mapViewRef.current.layer = undefined;
444 }
445 });
446 layerControl.on('layerToggle', (e) => {
447 const layerState = e.detail;
448 if (!layerState.checked) {
449 mapSettingsRef.current.hiddenLayers.push(layerState.key);
450 } else {
451 mapViewRef.current.layer = layerState.key;
452 mapSettingsRef.current.hiddenLayers = mapSettingsRef.current.hiddenLayers.filter(key => key !== layerState.key);
453 }
454 updateSavedMapSettings();
455 });
456 layerControl.on('groupToggle', (e) => {
457 const groupState = e.detail;
458 for (const groupLayer of layerControl._layers) {
459 if (groupLayer.group?.key !== groupState.key) {
460 continue;
461 }
462 if (!groupState.checked) {
463 mapSettingsRef.current.hiddenLayers.push(groupLayer.key);
464 } else {
465 mapSettingsRef.current.hiddenLayers = mapSettingsRef.current.hiddenLayers.filter(key => key !== groupLayer.key);
466 }
467 }
468 if (!groupState.checked) {
469 mapSettingsRef.current.hiddenGroups.push(groupState.key);
470 } else {
471 mapSettingsRef.current.hiddenGroups = mapSettingsRef.current.hiddenGroups.filter(key => key !== groupState.key);
472 }
473 updateSavedMapSettings();
474 });
475 layerControl.on('groupCollapseToggle', (e) => {
476 const groupState = e.detail;
477 if (groupState.collapsed) {
478 mapSettingsRef.current.collapsedGroups.push(groupState.key);
479
480 } else {
481 mapSettingsRef.current.collapsedGroups = mapSettingsRef.current.collapsedGroups.filter(key => key !== groupState.key);
482 }
483 updateSavedMapSettings();
484 });
485
486 const getLayerOptions = (layerKey, groupKey, layerName) => {
487 return {
488 groupKey,
489 layerKey,
490 groupName: groupKey,
491 layerName: layerName || categories[layerKey] || layerKey,
492 //groupHidden: Boolean(mapSettingsRef.current.hiddenGroups?.includes(groupKey)),
493 //layerHidden: Boolean(mapSettingsRef.current.hiddenLayers?.includes(layerKey)),
494 image: images[layerKey] ? "/static/maps/interactive/${images[layerKey]}.png" : undefined,
495 //groupCollapsed: Boolean(mapSettingsRef.current.collapsedGroups?.includes(groupKey)),
496 };
497 };
498
499 const addLayer = (layer, layerKey, groupKey, layerName) => {
500 layer.key = layerKey;
501 const layerOptions = getLayerOptions(layerKey, groupKey, layerName);
502 if (!layerOptions.layerHidden) {
503 layer.addTo(map);
504 }
505 layerControl.addOverlay(layer, layerOptions.layerName, layerOptions);
506 };
507 map.layerControl = layerControl;
508
509 // Hinzufügen des Image-Overlays
510 const overlay = L.imageOverlay(imageUrl, getBounds(svgBounds));
511 overlay.addTo(map);
512
513 function checkMarkerBounds(position, markerBounds) {
514 if (position.x < markerBounds.TL.x) markerBounds.TL.x = position.x;
515 if (position.z > markerBounds.TL.z) markerBounds.TL.z = position.z;
516 if (position.x > markerBounds.BR.x) markerBounds.BR.x = position.x;
517 if (position.z < markerBounds.BR.z) markerBounds.BR.z = position.z;
518 }
519
520 function getBounds(bounds) {
521 if (!bounds) {
522 return undefined;
523 }
524 return L.latLngBounds([bounds[0][1], bounds[0][0]], [bounds[1][1], bounds[1][0]]);
525 //return [[bounds[0][1], bounds[0][0]], [bounds[1][1], bounds[1][0]]];
526 }
527
528 function mouseHoverOutline(event) {
529 const outline = event.target.options.outline;
530 if (event.originalEvent.type === 'mouseover') {
531 outline._path.classList.remove('not-shown');
532 } else if (!outline._path.classList.contains('force-show')) {
533 outline._path.classList.add('not-shown');
534 }
535 }
536
537 function toggleForceOutline(event) {
538 const outline = event.target.options.outline;
539 outline._path.classList.toggle('force-show');
540 if (outline._path.classList.contains('force-show')) {
541 outline._path.classList.remove('not-shown');
542 }
543 activateMarkerLayer(event);
544 }
545
546 function activateMarkerLayer(event) {
547 const marker = event.target || event;
548 if (markerIsOnActiveLayer(marker)) {
549 return;
550 }
551 const activeLayers = Object.values(marker._map._layers).filter(l => l.options?.extents && l.options?.overlay);
552 for (const layer of activeLayers) {
553 layer.removeFrom(marker._map);
554 }
555 const heightLayers = marker._map.layerControl._layers.filter(l => l.layer.options.extents && l.layer.options.overlay).map(l => l.layer);
556 for (const layer of heightLayers) {
557 if (markerIsOnLayer(marker, layer)) {
558 layer.addTo(marker._map);
559 break;
560 }
561 }
562 }
563
564 function outlineToPoly(outline) {
565 if (!outline) return [];
566 return outline.map(vector => [vector.z, vector.x]);
567 }
568
569 const layerOptions = {
570 maxZoom: maxZoom,
571 maxNativeZoom: maxZoom,
572 extents: [
573 {
574 height: [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER],
575 bounds: [bounds],
576 }
577 ],
578 type: 'map-layer',
579 };
580 let tileLayer = false;
581 const baseLayers = [];
582 const tileSize = 256;
583 let svgLayer = false;
584 if (svgPath) {
585 const svgBounds2 = svgBounds ? getBounds(svgBounds) : bounds;
586 svgLayer = L.imageOverlay(svgPath, svgBounds2, layerOptions);
587 baseLayers.push(svgLayer);
588 }
589
590 const positionIsInBounds = (position) => {
591 return getBounds(bounds).contains(pos(position));
592 };
593
594 let markerBounds = {
595 'TL': { x: Number.MAX_SAFE_INTEGER, z: Number.MIN_SAFE_INTEGER },
596 'BR': { x: Number.MIN_SAFE_INTEGER, z: Number.MAX_SAFE_INTEGER }
597 }
598
599 if (mapData.labels?.length > 0) {
600 const labelsGroup = L.layerGroup();
601 const defaultHeight = ((layerOptions.extents[0].height[1] - layerOptions.extents[0].height[0]) / 2) + layerOptions.extents[0].height[0];
602 for (const label of mapData.labels) {
603 const fontSize = label.size ? label.size : 100;
604 const height = label.position.length < 3 ? defaultHeight : label.position[2];
605 const rotation = label.rotation ? label.rotation : 0;
606 L.marker(pos({ x: label.position[0], z: label.position[1] }), {
607 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, }),
608 interactive: false,
609 zIndexOffset: -100000,
610 position: {
611 x: label.position[0],
612 y: height,
613 z: label.position[1],
614 },
615 top: typeof label.top !== 'undefined' ? label.top : 1000,
616 bottom: typeof label.bottom !== 'undefined' ? label.bottom : -1000,
617 }).addTo(labelsGroup);
618 }
619 addLayer(labelsGroup, 'place-names', 'Landmarks');
620 //labelsGroup.addTo(map);
621 }
622
623 // Add spawns
624 if (mapData.spawns.length > 0) {
625 const spawnLayers = {
626 'pmc': L.layerGroup(),
627 'scav': L.layerGroup(),
628 'sniper_scav': L.layerGroup(),
629 'boss': L.layerGroup(),
630 'cultist-priest': L.layerGroup(),
631 'rogue': L.layerGroup(),
632 'bloodhound': L.layerGroup(),
633 }
634 for (const spawn of mapData.spawns) {
635 if (!positionIsInBounds(spawn.position)) {
636 continue;
637 }
638 let spawnType = '';
639 let bosses = [];
640
641 if (spawn.categories.includes('boss')) {
642 bosses = mapData.bosses.filter(boss => boss.spawnLocations.some(sl => sl.spawnKey === spawn.zoneName));
643 if (bosses.length === 0) {
644 if (spawn.categories.includes('bot') && spawn.sides.includes('scav')) {
645 spawnType = 'scav';
646 }
647 else {
648 console.error(`Unusual spawn: ${spawn.sides}, ${spawn.categories}`);
649 continue;
650 }
651 }
652 else if (bosses.length === 1 && (bosses[0].normalizedName === 'bloodhound' || bosses[0].normalizedName === 'cultist-priest' || bosses[0].normalizedName === 'rogue')) {
653 spawnType = bosses[0].normalizedName;
654 }
655 else {
656 spawnType = 'boss';
657 }
658 } else if (spawn.categories.includes('sniper')) {
659 spawnType = 'sniper_scav';
660 } else if (spawn.sides.includes('scav')) {
661 if (spawn.categories.includes('bot') || spawn.categories.includes('all')) {
662 spawnType = 'scav';
663 }
664 else {
665 console.error(`Unusual spawn: ${spawn.sides}, ${spawn.categories}`);
666 continue;
667 }
668 }
669 else if (spawn.categories.includes('player')) {
670 if (spawn.sides.includes('pmc') || spawn.sides.includes('all')) {
671 spawnType = 'pmc'
672 }
673 else {
674 console.error(`Unusual spawn: ${spawn.sides}, ${spawn.categories}`);
675 continue;
676 }
677 }
678 else {
679 console.error(`Unusual spawn: ${spawn.sides}, ${spawn.categories}`);
680 continue;
681 }
682
683 const spawnIcon = L.icon({
684 iconUrl: `/static/maps/interactive/spawn_${spawnType}.png`,
685 iconSize: [24, 24],
686 popupAnchor: [0, -12],
687 });
688
689 if (spawnType === 'pmc') {
690 spawnIcon.iconAnchor = [12, 24];
691 spawnIcon.popupAnchor = [0, -24];
692 }
693
694 const popupContent = L.DomUtil.create('div')
695 if (spawn.categories.includes('boss') && bosses.length > 0) {
696 bosses = bosses.reduce((unique, current) => {
697 if (!unique.some(b => b.normalizedName === current.normalizedName)) {
698 unique.push(current)
699 if (!categories[`spawn_${current.normalizedName}`]) {
700 categories[`spawn_${current.normalizedName}`] = current.name
701 }
702 }
703 return unique;
704 }, []);
705 const bossList = L.DomUtil.create('div', undefined, popupContent)
706 for (const boss of bosses) {
707 if (bossList.childNodes.length > 0) {
708 const comma = L.DomUtil.create('span', undefined, bossList)
709 comma.textContent = ', '
710 }
711 bossList.append(getALink(`https://escapefromtarkov.fandom.com/wiki/Special:Search?scope=internal&query=${boss.name}`, `${boss.name} (${Math.round(boss.spawnChance * 100)}%)`))
712 }
713 }
714 else {
715 const spawnDiv = L.DomUtil.create('div', undefined, popupContent)
716 spawnDiv.textContent = categories[`spawn_${spawnType}`]
717 }
718 addElevation(spawn, popupContent)
719
720 const marker = L.marker(pos(spawn.position), {
721 icon: spawnIcon,
722 position: spawn.position,
723 });
724 if (popupContent.childNodes.length > 0) {
725 marker.bindPopup(L.popup().setContent(popupContent))
726 }
727 marker.position = spawn.position
728 marker.on('add', checkMarkerForActiveLayers)
729 marker.on('click', activateMarkerLayer)
730 marker.addTo(spawnLayers[spawnType])
731
732 checkMarkerBounds(spawn.position, markerBounds)
733 }
734 for (const key in spawnLayers) {
735 if (Object.keys(spawnLayers[key]._layers).length > 0) {
736 addLayer(spawnLayers[key], `spawn_${key}`, 'Spawns')
737 }
738 }
739 }
740
741 //add extracts
742 if (mapData.extracts.length > 0) {
743 const extractLayers = {
744 pmc: L.layerGroup(),
745 scav: L.layerGroup(),
746 shared: L.layerGroup(),
747 }
748 const zIndexOffsets = {
749 pmc: 150,
750 shared: 125,
751 scav: 100,
752 };
753 for (const extract of mapData.extracts) {
754 const faction = extract.faction ?? 'shared';
755 if (!positionIsInBounds(extract.position)) {
756 //continue;
757 }
758 const colorMap = {
759 scav: '#ff7800',
760 pmc: '#00e599',
761 shared: '#00e4e5',
762 }
763 const rect = L.polygon(outlineToPoly(extract.outline), { color: colorMap[faction], weight: 1, className: 'not-shown' });
764 const extractIcon = L.divIcon({
765 className: 'extract-icon',
766 html: `<img src="/static/maps/interactive/extract_${faction}.png"/><span class="extract-name ${faction}">${extract.name}</span>`,
767 iconAnchor: [12, 12]
768 });
769 const extractMarker = L.marker(pos(extract.position), {
770 icon: extractIcon,
771 title: extract.name,
772 zIndexOffset: zIndexOffsets[faction],
773 position: extract.position,
774 top: extract.top,
775 bottom: extract.bottom,
776 outline: rect,
777 id: extract.id,
778 });
779 extractMarker.on('mouseover', mouseHoverOutline);
780 extractMarker.on('mouseout', mouseHoverOutline);
781 extractMarker.on('click', toggleForceOutline);
782 if (extract.switches?.length > 0) {
783 const popup = L.DomUtil.create('div');
784 const textElement = L.DomUtil.create('div');
785 textElement.textContent = `${tMaps('Activated by')}:`;
786 popup.appendChild(textElement);
787 for (const sw of extract.switches) {
788 const linkElement = getPoiLinkElement(sw.id, 'switch');
789 const nameElement = L.DomUtil.create('span');
790 nameElement.innerHTML = `<strong>${sw.name}</strong>`;
791 linkElement.append(nameElement);
792 popup.appendChild(linkElement);
793 }
794 addElevation(extract, popup);
795 extractMarker.bindPopup(L.popup().setContent(popup));
796 } else if (showElevation) {
797 const popup = L.DomUtil.create('div');
798 addElevation(extract, popup);
799 extractMarker.bindPopup(L.popup().setContent(popup));
800 }
801 extractMarker.on('add', checkMarkerForActiveLayers);
802 L.layerGroup([rect, extractMarker]).addTo(extractLayers[faction]);
803
804 checkMarkerBounds(extract.position, markerBounds);
805 }
806 if (mapData.transits.length > 0) {
807 extractLayers.transit = L.layerGroup();
808
809 for (const transit of mapData.transits) {
810 if (!positionIsInBounds(transit.position)) {
811 //continue;
812 }
813 const rect = L.polygon(outlineToPoly(transit.outline), { color: '#e53500', weight: 1, className: 'not-shown' });
814 const transitIcon = L.divIcon({
815 className: 'extract-icon',
816 html: `<img src="/static/maps/interactive/extract_transit.png"/><span class="extract-name transit">${transit.description}</span>`,
817 iconAnchor: [12, 12]
818 });
819 const transitMarker = L.marker(pos(transit.position), {
820 icon: transitIcon,
821 title: transit.description,
822 zIndexOffset: zIndexOffsets.pmc,
823 position: transit.position,
824 top: transit.top,
825 bottom: transit.bottom,
826 outline: rect,
827 id: transit.id,
828 });
829 transitMarker.on('mouseover', mouseHoverOutline);
830 transitMarker.on('mouseout', mouseHoverOutline);
831 transitMarker.on('click', toggleForceOutline);
832 if (showElevation) {
833 const popup = L.DomUtil.create('div');
834 addElevation(transit, popup);
835 transitMarker.bindPopup(L.popup().setContent(popup));
836 }
837 transitMarker.on('add', checkMarkerForActiveLayers);
838 L.layerGroup([rect, transitMarker]).addTo(extractLayers.transit);
839
840 checkMarkerBounds(transit.position, markerBounds);
841 }
842 }
843 for (const key in extractLayers) {
844 if (Object.keys(extractLayers[key]._layers).length > 0) {
845 addLayer(extractLayers[key], `extract_${key}`, 'Extracts');
846 }
847 }
848 }
849
850 // Add static items
851 if (showStaticMarkers) {
852 for (const category in mapData) {
853 const markerLayer = L.layerGroup();
854
855 const items = mapData[category];
856 for (const item of items) {
857 const itemIcon = L.icon({
858 iconUrl: `/static/maps/interactive/${category}.png`,
859 iconSize: [24, 24],
860 popupAnchor: [0, -12],
861 //className: layerIncludesMarker(heightLayer, item) ? '' : 'off-level',
862 });
863 L.marker(pos(item.position), { icon: itemIcon, position: item.position })
864 .bindPopup(L.popup().setContent(`${item.name}<br>Elevation: ${item.position.y}`))
865 .addTo(markerLayer);
866
867 checkMarkerBounds(item.position, markerBounds);
868 }
869
870 if (items.length > 0) {
871 var section;
872 if (category.startsWith('extract')) {
873 section = 'Extracts';
874 }
875 else if (category.startsWith('spawn')) {
876 section = 'Spawns';
877 }
878 else {
879 section = 'Lootable Items';
880 }
881 markerLayer.addTo(map);
882 addLayer(markerLayer, category, section);
883 // layerControl.addOverlay(markerLayer, `<img src='${process.env.PUBLIC_URL}/maps/interactive/${category}.png' class='control-item-image' /> ${categories[category] || category}`, section);
884 }
885 }
886 }
887
888 // Erstelle ein neues Control-Element
889 const customControl = L.Control.extend({
890 onAdd: function (map) {
891 this.container = L.DomUtil.create('div', 'custom-control leaflet-control-layers leaflet-control-layers-expanded')
892 this.container.innerHTML = '<h2>Add Marker to Pos: </h2>'
893 return this.container
894 },
895
896 updateText: function (html) {
897 if (this.container) {
898 this.container.innerHTML = html
899 }
900 },
901
902 clearText: function () {
903 if (this.container) {
904 this.container.innerHTML = ''
905 }
906 },
907
908 onRemove: function (map) {
909 // Nichts zu tun hier
910 }
911 });
912 const myControl = new customControl({ position: 'topright' })
913 map.addControl(myControl)
914
915 function startCountdown(elementId) {
916 const element = document.getElementById(elementId);
917 if (!element) {
918 console.error(`Element with id ${elementId} not found.`);
919 return;
920 }
921
922 //let secondsStart = Math.floor(new Date().getTime() / 1000.0) - parseInt(element.getAttribute('data-rade-end')) - 60
923 let secondsStart = parseInt(element.getAttribute('data-seconds')) - 60
924 let seconds = secondsStart
925 if (isNaN(secondsStart)) {
926 console.error('Invalid data-seconds attribute.');
927 return;
928 }
929
930 const intervalId = setInterval(() => {
931 if (!document.hidden) {
932 //seconds = (secondsStart - parseInt(Math.floor(new Date().getTime() / 1000.0)))
933 //localStorage.setItem('rade_data', JSON.stringify({ remaining_time: seconds }));
934 seconds--
935
936 if (seconds <= 0) {
937 clearInterval(intervalId);
938 element.textContent = '0:00:00';
939 myControl.updateText("")
940 document.querySelector("head > title").textContent = `${mapData.name} | Map with friends`
941 return;
942 }
943
944 //seconds--;
945 const hours = Math.floor(seconds / 3600);
946 const minutes = Math.floor((seconds % 3600) / 60);
947 const remainingSeconds = seconds % 60;
948
949 const timeString = `${hours}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
950 element.textContent = timeString;
951 document.querySelector("head > title").textContent = `${mapData.name} | ${timeString}`
952 }
953 }, 1000);
954 }
955
956 function addNewRadeData(new_html) {
957 myControl.updateText(new_html)
958 startCountdown("rade_time_remain")
959 };
960
961 // Funktion zum Hinzufügen eines Markers
962 function addMarker(x, y, z, timestamp, preview = false) {
963 // Position validieren
964 const position = {
965 x: parseFloat(x),
966 y: parseFloat(y),
967 z: parseFloat(z)
968 };
969
970 if (!positionIsInBounds(position)) {
971 console.error("Position außerhalb der Karte:", position);
972 return;
973 }
974
975 // SVG-Icon als String mit dynamischer Farbe
976 const svgString = `
977 <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 384 512' class="marker-svg">
978 <path fill='currentColor' 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'/>
979 </svg>
980 `;
981
982 // DivIcon mit dynamischem SVG erstellen
983 const markerIcon = L.divIcon({
984 html: svgString,
985 className: 'custom-marker',
986 iconSize: [32, 32],
987 iconAnchor: [16, 32], // Spitze des Markers zeigt auf die Position
988 popupAnchor: [0, -32]
989 });
990
991 // Alten Marker entfernen
992 if (currentMarker) {
993 map.removeLayer(currentMarker);
994 }
995
996 // Neuen Marker erstellen
997 currentMarker = L.marker(pos(position), {
998 icon: markerIcon,
999 position: position,
1000 title: `Koordinaten: ${x}, ${y}, ${z}`,
1001 riseOnHover: true
1002 });
1003
1004 // Popup mit Informationen erstellen
1005 const popupContent = `
1006 <div class="marker-popup">
1007 <strong>Koordinaten:</strong>
1008 <span>X: ${x} Y: ${y} Z: ${z}</span>
1009 <img class="preview-image" src="${preview}"><br>
1010 <small>${timestamp}</small>
1011 </div>
1012 `;
1013
1014 currentMarker.bindPopup(popupContent, {
1015 maxWidth: 250,
1016 minWidth: 150,
1017 autoClose: true,
1018 closeOnClick: true
1019 });
1020
1021 // Marker zur Karte hinzufügen
1022 currentMarker.addTo(map);
1023
1024 // Karte auf Marker zentrieren
1025 map.setView(pos(position), map.getZoom(), {
1026 animate: true,
1027 duration: 0.5
1028 });
1029
1030 console.log("Neuer Marker gesetzt:", position);
1031 }
1032
1033 document.onload = loadLocalData()
1034
1035 // BULMA JAVASCRIPTS
1036 document.addEventListener('DOMContentLoaded', () => {
1037
1038 // Get all "navbar-burger" elements
1039 const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
1040
1041 // Add a click event on each of them
1042 $navbarBurgers.forEach(el => {
1043 el.addEventListener('click', () => {
1044
1045 // Get the target from the "data-target" attribute
1046 const target = el.dataset.target;
1047 const $target = document.getElementById(target);
1048
1049 // Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
1050 el.classList.toggle('is-active');
1051 $target.classList.toggle('is-active');
1052
1053 });
1054 });
1055
1056 });
1057
1058 // MODAL Trigger
1059 document.addEventListener('DOMContentLoaded', () => {
1060 // Functions to open and close a modal
1061 function openModal($el) {
1062 $el.classList.add('is-active');
1063 }
1064
1065 function closeModal($el) {
1066 $el.classList.remove('is-active');
1067 }
1068
1069 function closeAllModals() {
1070 (document.querySelectorAll('.modal') || []).forEach(($modal) => {
1071 closeModal($modal);
1072 });
1073 }
1074
1075 // Add a click event on buttons to open a specific modal
1076 (document.querySelectorAll('.js-modal-trigger') || []).forEach(($trigger) => {
1077 const modal = $trigger.dataset.target;
1078 const $target = document.getElementById(modal);
1079
1080 $trigger.addEventListener('click', () => {
1081 openModal($target);
1082 });
1083 });
1084
1085 // Add a click event on various child elements to close the parent modal
1086 (document.querySelectorAll('.modal-background, .modal-close, .modal-card-head .delete, .modal-card-foot .button') || []).forEach(($close) => {
1087 const $target = $close.closest('.modal');
1088
1089 $close.addEventListener('click', () => {
1090 closeModal($target);
1091 });
1092 });
1093
1094 // Add a keyboard event to close all modals
1095 document.addEventListener('keydown', (event) => {
1096 if (event.key === "Escape") {
1097 closeAllModals();
1098 }
1099 });
1100 });
1101 </script>
1102 </body>
1103</html>
1104 