CoolFace
Apppublic

Sebastiankay/eft_group_map_websocket

sourceHugging Facemitupdated 1y agoView on Hugging Face
0likes
map.backup2.html1104 linesDownload Raw Back to templates
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