// ============================================================ // FC Map // ============================================================ var fcLeafletMap = null; var _lastSectionH = 0; var _mapIntroShown = false; function sizeMapSection() { var header = document.querySelector('.section-header'); var headerH = header ? header.getBoundingClientRect().height : 0; var section = document.querySelector('.section-map'); var newH = window.innerHeight - headerH; if (section) { if (!_mapIntroShown) { _mapIntroShown = true; // Start from 0 height, animate to full height section.style.height = '0px'; section.style.overflow = 'hidden'; section.style.transition = 'height 0.8s cubic-bezier(0.4,0,0.2,1)'; // rAF ensures the 0px is painted first requestAnimationFrame(function() { requestAnimationFrame(function() { section.style.height = newH + 'px'; section.addEventListener('transitionend', function onSlideEnd() { section.removeEventListener('transitionend', onSlideEnd); section.style.overflow = ''; section.style.transition = ''; if (fcLeafletMap) { fcLeafletMap.invalidateSize(); fcLeafletMap.fitCoverFn && fcLeafletMap.fitCoverFn(false); } }, { once: true }); }); }); // After 3s, fade out and remove the intro overlay setTimeout(function() { var intro = document.getElementById('fc-map-intro'); if (intro) { intro.classList.add('is-hidden'); intro.addEventListener('transitionend', function() { intro.parentNode && intro.parentNode.removeChild(intro); }, { once: true }); } }, 3000); } else { section.style.height = newH + 'px'; } } if (fcLeafletMap) { fcLeafletMap.invalidateSize(); fcLeafletMap.fitCoverFn && fcLeafletMap.fitCoverFn(false); _lastSectionH = newH; } } document.addEventListener('DOMContentLoaded', sizeMapSection); window.addEventListener('resize', sizeMapSection); (function() { if (!document.getElementById('fc-map')) return; var IMG_SRC = 'https://famacollection.org/layout/fc-map-main.jpg'; // ───────────────────────────────────────────────────────────────── // PINS — dodajte/editujte ovdje // lat/lng su koordinate u pikselima slike (y od dna, x od lijeva) // Koristite konzolu: map.on('click', function(e){console.log(e.latlng);}) // pa zakomentirajte taj debug listener kad završite. // ───────────────────────────────────────────────────────────────── var SPECIFIC_DATA = [ { x: 276, y: 1677, title: 'Aerodrom', slug: 'airport' }, { x: 1538, y: 1139, title: 'Fabrika duhana', slug: 'tobacco-factory' }, { x: 1382, y: 957, title: 'Hotel Holiday Inn ', slug: 'holiday-inn-hotel' }, { x: 2083, y: 1271, title: 'Narodno pozorište', slug: 'national-theatre' }, { x: 2300, y: 887, title: 'Olimpijski muzej', slug: 'olympic-museum' }, { x: 2335, y: 1073, title: 'Pijaca Markale', slug: 'markale' }, { x: 2534, y: 1619, title: 'Pivara', slug: 'brewery' }, { x: 1927, y: 214, title: 'Porodilište', slug: 'maternity-hospital' }, { x: 1980, y: 1244, title: 'Pošta', slug: 'post-office' }, { x: 175, y: 1789, title: 'Tunel', slug: 'tunnel' }, { x: 877, y: 710, title: 'Velepekara', slug: 'city-bakery' }, { x: 2600, y: 1390, title: 'Vijećnica', slug: 'city-hall' }, { x: 1339, y: 1058, title: 'Zemaljski muzej', slug: 'national-museum' }, { x: 1869, y: 467, title: 'Zetra', slug: 'zetra' }, { x: 1063, y: 988, title: 'Zgrada Elektroprivrede', slug: 'elektroprivreda' }, { x: 152, y: 741, title: 'Zgrada Oslobođenja', slug: 'oslobodjenje' }, { x: 1848, y: 1100, title: 'Zgrada Predsjedništva', slug: 'presidency_' }, { x: 460, y: 715, title: 'Zgrada radio-televizije', slug: 'radio-television' }, { x: 1447, y: 721, title: 'Željeznička stanica', slug: 'railway-station' }, ]; // Neutralne lokacije — grupisane. // Svaka grupa daje title (prikaz u listi), slug (modal), // i coords: niz koordinata za pinove na karti. // Tematske lokacije — isti princip kao GROUP_DATA. var THEMATIC_DATA = [{ title: 'Voda', slug: 'water', coords: [ { x: 2454, y: 1641 }, { x: 2105, y: 1515 }, { x: 2235, y: 1286 }, { x: 1348, y: 888 }, { x: 823, y: 959 }, { x: 566, y: 1001 }, { x: 780, y: 783 }, { x: 1016, y: 759 } ] }, { title: 'Struja', slug: 'electricity', coords: [ { x: 269, y: 223 }, { x: 428, y: 320 }, { x: 679, y: 464 } ] }, { title: 'Grijanje', slug: 'heating', coords: [ { x: 1027, y: 666 }, { x: 1179, y: 549 }, { x: 650, y: 560 }, { x: 1977, y: 935 }, { x: 1967, y: 1529 }, { x: 2502, y: 1563 }, { x: 2336, y: 788 } ] }, { title: 'Hrana', slug: 'food', coords: [ { x: 2188, y: 1426 }, { x: 2307, y: 1510 }, { x: 1853, y: 710 }, { x: 1783, y: 899 }, { x: 716, y: 990 }, { x: 431, y: 865 } ] }, { title: 'Umjetnost', slug: 'art', coords: [ { x: 719, y: 825 }, { x: 1917, y: 1309 }, { x: 2041, y: 1270 }, { x: 1783, y: 1314 }, { x: 1540, y: 1320 }, { x: 2226, y: 1324 } ] }, { title: 'Film', slug: 'film', coords: [ { x: 1752, y: 997 } ] }, { title: 'Izrada objekata', slug: 'building-objects', coords: [ { x: 2218, y: 930 }, { x: 2227, y: 1312 } ] }, { title: 'Škole', slug: 'schools', coords: [ { x: 510, y: 904 }, { x: 590, y: 940 }, { x: 861, y: 1051 }, { x: 1651, y: 787 }, { x: 2120, y: 1034 }, { x: 2251, y: 1073 }, { x: 2443, y: 1235 }, { x: 2553, y: 1234 }, { x: 2338, y: 1352 }, { x: 2058, y: 1183 }, { x: 1928, y: 1472 }, { x: 1723, y: 1404 } ] }, { title: 'Društvo', slug: 'society', coords: [ { x: 1548, y: 948 }, { x: 1664, y: 696 }, { x: 2078, y: 1165 }, { x: 2041, y: 858 }, { x: 1932, y: 655 }, { x: 2413, y: 960 }, { x: 2465, y: 1226 }, { x: 2362, y: 1379 }, { x: 2267, y: 1614 }, { x: 1933, y: 1457 }, { x: 1157, y: 885 }, { x: 803, y: 823 }, { x: 463, y: 839 }, { x: 210, y: 863 }, { x: 346, y: 978 }, { x: 525, y: 952 }, { x: 643, y: 1050 }, { x: 892, y: 962 }, { x: 1028, y: 1063 } ] }, ]; var GROUP_DATA = [{ title: 'Mostovi', slug: 'bridges', coords: [ { x: 724, y: 912 }, { x: 825, y: 998 }, { x: 975, y: 1026 }, { x: 1100, y: 1064 }, { x: 1148, y: 1078 }, { x: 1446, y: 1185 }, { x: 1628, y: 1204 }, { x: 1731, y: 1243 }, { x: 1991, y: 1327 }, { x: 2145, y: 1374 }, { x: 2273, y: 1412 }, { x: 2379, y: 1445 }, { x: 2457, y: 1462 }, { x: 2664, y: 1530 } ] }, { title: 'Groblja', slug: 'cemeteries', coords: [ { x: 2493, y: 809 }, { x: 1932, y: 507 }, { x: 2095, y: 390 } ] }, { title: 'Bašte', slug: 'gardens', coords: [ { x: 1824, y: 713 }, { x: 1823, y: 880 }, { x: 2283, y: 1510 } ] }, { title: 'Bolnice', slug: 'hospitals', coords: [ { x: 2233, y: 525 }, { x: 1601, y: 864 }, { x: 1173, y: 787 } ] }, { title: 'Pijace', slug: 'markets', coords: [ { x: 1833, y: 647 }, { x: 2298, y: 1073 }, { x: 717, y: 943 } ] }, { title: 'Parkovi', slug: 'parks', coords: [ { x: 1874, y: 1007 }, { x: 1981, y: 951 }, { x: 717, y: 943 }, { x: 1764, y: 907 }, { x: 2335, y: 1512 }, { x: 2495, y: 1562 } ] }, { title: 'Zaštita', slug: 'protection', coords: [ { x: 285, y: 934 }, { x: 279, y: 1064 }, { x: 961, y: 814 }, { x: 1091, y: 874 }, { x: 1281, y: 870 }, { x: 1443, y: 893 }, { x: 1590, y: 993 }, { x: 1582, y: 1235 }, { x: 2106, y: 1163 }, { x: 1659, y: 1103 } ] }, { title: 'Opasne zone', slug: 'dangerous-zones', coords: [ { x: 881, y: 1062 }, { x: 897, y: 888 }, { x: 1042, y: 914 }, { x: 1188, y: 983 }, { x: 1435, y: 963 }, { x: 1565, y: 943 }, { x: 1511, y: 1072 }, { x: 1657, y: 1041 }, { x: 1641, y: 1167 }, { x: 1987, y: 1109 }, { x: 2011, y: 1304 }, { x: 2115, y: 1148 }, { x: 2172, y: 942 }, { x: 2664, y: 1468 }, { x: 2592, y: 1260 }, { x: 643, y: 854 }, { x: 293, y: 768 } ] }, { title: 'Snajperi', slug: 'snipers', coords: [ { x: 392, y: 805 }, { x: 282, y: 894 }, { x: 591, y: 857 }, { x: 736, y: 889 }, { x: 882, y: 914 }, { x: 1006, y: 945 }, { x: 1124, y: 841 }, { x: 1269, y: 899 }, { x: 1201, y: 998 }, { x: 1320, y: 1016 }, { x: 1540, y: 740 }, { x: 1444, y: 875 }, { x: 1438, y: 913 }, { x: 1451, y: 998 }, { x: 1396, y: 1043 }, { x: 1457, y: 1168 }, { x: 1505, y: 1098 }, { x: 1529, y: 1052 }, { x: 1752, y: 694 }, { x: 1737, y: 803 }, { x: 1699, y: 1026 }, { x: 1655, y: 1050 }, { x: 1669, y: 1076 }, { x: 1667, y: 1141 }, { x: 1627, y: 1180 }, { x: 1738, y: 1218 }, { x: 1897, y: 771 }, { x: 1904, y: 899 }, { x: 1911, y: 1064 }, { x: 1928, y: 1106 }, { x: 1948, y: 1200 }, { x: 1998, y: 1322 }, { x: 2055, y: 1241 }, { x: 2098, y: 1183 }, { x: 2129, y: 1131 }, { x: 2185, y: 953 }, { x: 2149, y: 1377 }, { x: 2284, y: 1389 }, { x: 2391, y: 1438 }, { x: 2367, y: 1503 }, { x: 2659, y: 1566 }, { x: 2654, y: 1488 }, { x: 2594, y: 1220 }, { x: 2560, y: 1285 }, { x: 2436, y: 1091 } ] }, { title: 'Prevoz', slug: 'transportation', coords: [ { x: 1196, y: 754 }, { x: 686, y: 781 }, { x: 1710, y: 644 }, { x: 1632, y: 1188 }, { x: 1861, y: 1070 }, { x: 2720, y: 1426 }, { x: 1391, y: 828 }, { x: 701, y: 872 }, { x: 2244, y: 1291 }, { x: 1568, y: 1075 }, { x: 1014, y: 927 }, { x: 950, y: 913 } ] }, { title: 'UNPROFOR', slug: 'unprofor', coords: [ { x: 295, y: 697 }, { x: 1364, y: 823 }, { x: 1690, y: 857 }, { x: 1555, y: 1287 }, { x: 353, y: 1626 }, { x: 1820, y: 773 } ] }, ]; var SITE_DATA = [ // url: Google Maps link koji se otvara na klik pina ili u listi { x: 1499, y: 378, title: 'Hum', title_eng: 'Hum', url: 'https://maps.app.goo.gl/EnUxHTk69rZ29GL2A' }, { x: 1890, y: 413, title: 'Asim Ferhatović Hase (Koševo) Olimpijski stadion', title_eng: 'Asim Ferhatović Hase (Koševo) Olympic Stadium', url: 'https://maps.app.goo.gl/Ludnio5k3vpeSL9X6' }, { x: 1505, y: 989, title: 'UNIS (sada UNITIC) neboderi', title_eng: 'UNIS (now UNITIC) Skyscrapers', url: 'https://maps.app.goo.gl/G78UEBALZbDNxdsYA' }, { x: 2389, y: 1197, title: 'Katedrala Srca Isusova', title_eng: 'Sacred Heart Cathedral', url: 'https://maps.app.goo.gl/mFYfbHenvnNi81ar5' }, { x: 1990, y: 971, title: 'Veliki park', title_eng: 'Veliki park', url: 'https://maps.app.goo.gl/sN7fAYx8H9ggNagBA' }, { x: 1944, y: 1144, title: 'Robna kuća Sarajka (sada ARIA Mall)', title_eng: 'Department Store Sarajka (now ARIA Mall)', url: 'https://maps.app.goo.gl/LGQEgoGxwhRWcwCY7' }, { x: 1880, y: 1248, title: 'Pravni fakultet Sarajevo', title_eng: 'Faculty of Law Sarajevo', url: 'https://maps.app.goo.gl/Su2FTSCX83nVzpLTA' }, { x: 1858, y: 1372, title: 'Likovna akademija Sarajevo', title_eng: 'Academy of Fine Arts', url: 'https://maps.app.goo.gl/8Lq9EHC5o55VcrUo9' }, { x: 2174, y: 564, title: 'Bolnica Koševo (sada Klinički centar Univerziteta Sarajevo)', title_eng: 'Hospital Koševo (now Sarajevo University Clinical Centre)', url: 'https://maps.app.goo.gl/G8vguSVjEmnkAdaj9' }, { x: 2070, y: 428, title: 'Groblje Lav', title_eng: 'Cemetery Lav', url: 'https://maps.app.goo.gl/fCk9xWQQWA3nsWzV9' }, { x: 1661, y: 722, title: 'Ciglane', title_eng: 'Ciglane', url: 'https://maps.app.goo.gl/c2rByRQxGoT3Ua3N9' }, { x: 1609, y: 927, title: 'Vojna bolnica (sada Opća bolnica \'Prim. dr. Abdulah Nakaš\')', title_eng: 'Military Hospital (now General Hospital \'Prim. dr. Abdulah Nakaš\')', url: 'https://maps.app.goo.gl/uJuJubPFB6MjDNL76' }, { x: 1709, y: 1276, title: 'Ajfelov most', title_eng: 'Eiffel\'s Bridge', url: 'https://maps.app.goo.gl/6PZLDGoZ8Hqon8V37' }, { x: 1487, y: 1361, title: 'Centar Skenderija', title_eng: 'Centre Skenderija', url: 'https://maps.app.goo.gl/rRXhW3XHwSHLVcaC9' }, { x: 1974, y: 1370, title: 'Most Čobanija', title_eng: 'Čobanija Bridge', url: 'https://maps.app.goo.gl/4B9sm4grDcM1TXfd6' }, { x: 2191, y: 1494, title: 'Papagajka', title_eng: 'Papagajka Building', url: 'https://maps.app.goo.gl/mxgj3t5cyrVY9Z9J6' }, { x: 2396, y: 1566, title: 'Careva džamija', title_eng: 'Emperor\'s Mosque', url: 'https://maps.app.goo.gl/EYc5MSRZbbRXnhur6' }, { x: 2372, y: 1679, title: 'Crkva Sv. Ante Padovanskog', title_eng: 'St. Anthony Church', url: 'https://maps.app.goo.gl/2VaqGUSd2XKCMkFV9' }, { x: 2057, y: 1457, title: 'Aškenaska sinagoga', title_eng: 'Ashkenazi Synagogue', url: 'https://maps.app.goo.gl/iWf1Mk2Q8agT7bqW9' }, { x: 852, y: 1343, title: 'Spomen-park Vraca', title_eng: 'Memorial Park Vraca', url: 'https://maps.app.goo.gl/H2F4JJVZCSZqUyCr9' }, { x: 1332, y: 1343, title: 'Jevrejsko groblje', title_eng: 'Jewish Cemetery', url: 'https://maps.app.goo.gl/WB3vhSNx2NJfZjuX8' }, { x: 1437, y: 1214, title: 'Vrbanja most (sada Most Suade i Olge)', title_eng: 'Vrbanja Bridge (now Suada and Olga Bridge)', url: 'https://maps.app.goo.gl/VKL1unFyyQYSjrv78' }, { x: 1656, y: 1015, title: 'Crveni križ Sarajevo (nekada i Kino Sutjeska)', title_eng: 'Red Cross Sarajevo (once also Cinema Sutjeska)', url: 'https://maps.app.goo.gl/MZCdwPKrpwGJBwB76' }, { x: 1603, y: 1255, title: 'Most Skenderija', title_eng: 'Skenderija Bridge', url: 'https://maps.app.goo.gl/5Rs67XF6yvgVCSr97' }, { x: 2257, y: 1453, title: 'Most Ćumurija', title_eng: 'Ćumurija Bridge', url: 'https://maps.app.goo.gl/fdwMrcZeioPvKWev7' }, { x: 2138, y: 1412, title: 'Most Drvenija', title_eng: 'Drvenija Bridge', url: 'https://maps.app.goo.gl/hfwT58vwuxLfSrF19' }, { x: 2379, y: 1490, title: 'Latinska ćuprija', title_eng: 'Latin Bridge', url: 'https://maps.app.goo.gl/jXJCAZe2h37DRkmLA' }, { x: 2450, y: 1512, title: 'Careva ćuprija', title_eng: 'Emperor\'s Bridge', url: 'https://maps.app.goo.gl/zHzebUEx3M4AcFjn9' }, { x: 2646, y: 1552, title: 'Šeher Ćehajina Ćuprija', title_eng: 'Šeher Ćehaja Bridge', url: 'https://maps.app.goo.gl/6hCenQqXg7V3E9qQ7' }, { x: 2601, y: 1299, title: 'Sebilj', title_eng: 'Sebilj', url: 'https://maps.app.goo.gl/hc9mFv5X87rHEyYP9' }, { x: 2462, y: 1320, title: 'Sahat kula', title_eng: 'Clock Tower', url: 'https://maps.app.goo.gl/17HSfREXPNBHjRJf6' }, { x: 2293, y: 1299, title: 'Saborna crkva', title_eng: 'Orthodox Church', url: 'https://maps.app.goo.gl/THrz8LJNcQeTyBYk7' }, { x: 2086, y: 1109, title: 'Centralna banka', title_eng: 'Central Bank', url: 'https://maps.app.goo.gl/Emd2VhkRWS3iHjtv5' }, { x: 2248, y: 1104, title: 'Srednja muzička škola / Muzička akademija', title_eng: 'Music School / Music Academy', url: 'https://maps.app.goo.gl/6HhWwQ9BSouLzE5h7' }, { x: 1539, y: 1049, title: 'Tršćanska ulica (sada ul. Fra Anđela Zvizdovića)', title_eng: 'Tršćanska Street (now Fra Anđela Zvizdovića Street)', url: 'https://maps.app.goo.gl/6sdHyzcUg2AxGtbq9' }, { x: 1209, y: 909, title: 'Energoinvest', title_eng: 'Energoinvest', url: 'https://maps.app.goo.gl/sX7wDBTKbzHgVeSt9' }, { x: 1295, y: 972, title: 'Kasarna "Maršal Tito" (sada Kampus Univerziteta Sarajevo)', title_eng: '"Marshal Tito" military barracks (now Campus of the University of Sarajevo)', url: 'https://maps.app.goo.gl/fpci1tovRhejxJfv8' }, { x: 993, y: 1180, title: 'Grbavica', title_eng: 'Grbavica', url: 'https://maps.app.goo.gl/udBCc5HZn3dGLF6Y7' }, { x: 679, y: 823, title: 'Remiza (spremište za tramvaje)', title_eng: 'Tram depot', url: 'https://maps.app.goo.gl/ShoHUofM4NvWxYiW9' }, { x: 783, y: 941, title: 'Hrasno', title_eng: 'Hrasno', url: 'https://maps.app.goo.gl/Eb4wTBvZo4TAfi7R6' }, { x: 430, y: 912, title: 'Alipašino Polje', title_eng: 'Alipašino Polje', url: 'https://maps.app.goo.gl/Ts7ipsf42j1U4dVXA' }, { x: 611, y: 925, title: 'Otoka', title_eng: 'Otoka', url: 'https://maps.app.goo.gl/5XJecJV9YWu92cQF6' }, { x: 342, y: 980, title: 'Mojmilo', title_eng: 'Mojmilo', url: 'https://maps.app.goo.gl/4YyHrsRpBF6EvBjx7' }, { x: 1719, y: 1147, title: 'Skupština Grada Sarajeva (sada Općina Centar i Kanton Sarajevo)', title_eng: 'Sarajevo City Assembly (now Centre Municipality and Canton Sarajevo building)', url: 'https://maps.app.goo.gl/mbTcSwa956EmVQab9' }, { x: 250, y: 878, title: 'Vojničko Polje', title_eng: 'Vojničko Polje', url: 'https://maps.app.goo.gl/EdLGGzGUqbHq95C9A' }, { x: 2504, y: 1358, title: 'Gazi Husrev-begova džamija', title_eng: 'Gazi Husrev-beg Mosque', url: 'https://maps.app.goo.gl/JnwraeV4KQ1iz6BD6' }, { x: 2156, y: 1043, title: 'Dalmatinska ulica', title_eng: 'Dalmatinska street', url: 'https://maps.app.goo.gl/ywgLoJqwF6jHDLk89' }, { x: 2174, y: 943, title: 'Mejtaš', title_eng: 'Mejtaš', url: 'https://maps.app.goo.gl/rCBfKLNW6y3TA77u9' }, { x: 2215, y: 945, title: 'Dom izviđača (sada O.Š. Silvije Strahimir Kranjčević', title_eng: 'Scout Headquarters (now Primary School Silvije Strahimir Kranjčević', url: 'https://maps.app.goo.gl/T3x3FFKGNaPsTYZi8' }, { x: 1990, y: 912, title: 'Džidžikovac', title_eng: 'Džidžikovac', url: 'https://maps.app.goo.gl/iHnJc7y9EL71DXPK6' }, { x: 1654, y: 780, title: 'Radio ZID', title_eng: 'Radio ZID', url: 'https://maps.app.goo.gl/jHGcCKv26PpCoCyh9' }, { x: 280, y: 699, title: 'PTT Inžinjering', title_eng: 'PTT Building', url: 'https://maps.app.goo.gl/RTQfPfMxNTYxWZvLA' }, { x: 1558, y: 651, title: 'Tunel Ciglane', title_eng: 'Brick-yard Tunnel', url: 'https://maps.app.goo.gl/HxrZaCyt5VhYnS3b7' }, { x: 2563, y: 840, title: 'Višegradska kapija', title_eng: 'Višegrad Gate', url: 'https://maps.app.goo.gl/5KFaay9ejLmjVPAfA' }, { x: 1137, y: 1004, title: 'Hotel Bristol (sada Mövenpick Hotel)', title_eng: 'Hotel Bristol (now Mövenpick Hotel)', url: 'https://maps.app.goo.gl/d6sDxK6KT1jL9zhm8' }, { x: 1513, y: 1160, title: 'Fabrika duhana (sada SCC)', title_eng: 'Tobacco Factory (now SCC)', url: 'https://maps.app.goo.gl/EWcREsCT57gajS1k7' }, { x: 2288, y: 1195, title: 'Tržnica Markale', title_eng: 'Markale Market', url: 'https://maps.app.goo.gl/JLjB8rg6HxLwPzpq9' }, { x: 729, y: 846, title: 'Kentauri (premješteni)', title_eng: 'Centaurs (relocated)', url: 'https://maps.app.goo.gl/cedDjfPkzJxM5n316' }, { x: 2525, y: 982, title: 'Muzej Grada (sada Fakultet islamskih nauka)', title_eng: 'City Museum (now Faculty of Islamic Studies))', url: 'https://maps.app.goo.gl/EwuDmudQr6hrMTQr6' } ]; var map, mapBounds, w, h, coverZoom, mapImageOverlay, mapImageOverlayMono; var _allMarkers = []; function btnAnim(id) { var el = document.getElementById(id); el.classList.remove('btn-pulse'); requestAnimationFrame(function() {el.classList.add('btn-pulse');}); el.addEventListener('animationend', function() {el.classList.remove('btn-pulse');}, { once: true }); } var probe = new Image(); probe.onload = function() { initMap(this.naturalWidth, this.naturalHeight); }; probe.onerror = function() { initMap(4000, 2800); }; probe.src = IMG_SRC; function initMap(imgW, imgH) { w = imgW; h = imgH; mapBounds = [ [0, 0], [h, w] ]; map = L.map('fc-map', { crs: L.CRS.Simple, minZoom: -4, maxZoom: 4, zoomControl: false, attributionControl: false, zoomSnap: 0, scrollWheelZoom: false, keyboard: false, maxBounds: mapBounds, maxBoundsViscosity: 1.0 }); mapImageOverlay = L.imageOverlay(IMG_SRC, mapBounds, { opacity: 1 }).addTo(map); mapImageOverlayMono = L.imageOverlay('https://famacollection.org/layout/fc-map-gray.jpg', mapBounds, { opacity: 0 }).addTo(map); // Expose map and fitCover to the global sizeMapSection fcLeafletMap = map; fcLeafletMap.fitCoverFn = function(initial) { fitCover(initial !== false); }; // Size section first, then fit (ensures container has correct dimensions) sizeMapSection(); function updatePinScale() { document.getElementById('fc-map').classList.toggle('fc-map--cover', map.getZoom() <= coverZoom + 1.87); } function updateLabels() { var threshold = window.innerWidth <= 768 ? coverZoom + 3.0 : coverZoom + 2.0; document.getElementById('fc-map').classList.toggle('fc-map--labels', map.getZoom() > threshold); } map.on('zoomend', function() { updatePinScale(); updateLabels(); // Batch tooltip reposition in rAF so class changes are applied first requestAnimationFrame(function() { _allMarkers.forEach(function(m) { var tt = m.getTooltip(); if (tt) tt.update(); }); }); }); requestAnimationFrame(function() { fitCover(true); updatePinScale(); updateLabels(); }); // DEBUG — uključite da nađete koordinate, pa isključite: // map.on('click', function (e) {console.log(e.latlng);}); // Double right-click → zoom out (desktop only) var _lastRightClick = 0; map.getContainer().addEventListener('mousedown', function(e) { if (e.button !== 2) return; var now = Date.now(); if (now - _lastRightClick < 350) { var latlng = map.mouseEventToLatLng(e); map.flyTo(latlng, Math.max(map.getZoom() - 1, coverZoom - 0.75), { animate: true, duration: 0.4 }); _lastRightClick = 0; } else { _lastRightClick = now; } }); map.getContainer().addEventListener('contextmenu', function(e) {e.preventDefault();}); document.getElementById('fc-zoom-in').addEventListener('click', function() { var z = Math.min(map.getZoom() + 0.75, coverZoom + 3.75); map.flyTo(map.getCenter(), z, { animate: true, duration: 0.4 }); btnAnim('fc-zoom-in'); }); document.getElementById('fc-zoom-out').addEventListener('click', function() { var z = Math.max(map.getZoom() - 0.75, coverZoom - 0.75); map.flyTo(map.getCenter(), z, { animate: true, duration: 0.4 }); btnAnim('fc-zoom-out'); }); document.getElementById('fc-reset').addEventListener('click', function() { _prevView = null; document.getElementById('fc-overlay').classList.remove('is-open'); document.getElementById('fc-overlay-backdrop').classList.remove('is-open'); hideSpotlight(); // Compute cover target and flyTo it with animation var containerW = map.getContainer().offsetWidth; var containerH = map.getContainer().offsetHeight; var zoomW = Math.log2(containerW / w); var zoomH = Math.log2(containerH / h); var zoom = Math.max(zoomW, zoomH); coverZoom = zoom; var visibleH = containerH / Math.pow(2, zoom); var centerLat = h - visibleH / 2; map.flyTo([centerLat, w / 2], zoom, { animate: true, duration: 0.5 }); btnAnim('fc-reset'); }); SPECIFIC_DATA.forEach(function(pin) {addPin(pin);}); // Expand grouped GROUP_DATA into individual pins GROUP_DATA.forEach(function(group) { group.coords.forEach(function(coord) { var pin = { y: coord.y, x: coord.x, title: group.title, slug: group.slug, neutral: true, _group: group }; addPin(pin); group._pins = group._pins || []; group._pins.push(pin); }); }); THEMATIC_DATA.forEach(function(group) { group.coords.forEach(function(coord) { var pin = { y: coord.y, x: coord.x, title: group.title, slug: group.slug, thematic: true, _group: group }; addPin(pin); group._pins = group._pins || []; group._pins.push(pin); }); }); SITE_DATA.forEach(function(pin) { pin.site = true; addPin(pin); }); // Sort group _pins by x axis (left → right) GROUP_DATA.forEach(function(g) { if (g._pins) g._pins.sort(function(a, b) { return a.x - b.x; }); }); THEMATIC_DATA.forEach(function(g) { if (g._pins) g._pins.sort(function(a, b) { return a.x - b.x; }); }); // ── Mini-map init (needs map + coverZoom to be ready) ── (function() { var mm = document.getElementById('fc-minimap'); var mmRect = document.getElementById('fc-minimap-rect'); var MM_W = 200; function updateMinimap() { var visible = map.getZoom() > coverZoom + 0.15; mm.classList.toggle('is-visible', visible); if (!visible) return; var scale = MM_W / w; var b = map.getBounds(); var left = b.getWest() * scale; var top = (h - b.getNorth()) * scale; var rw = (b.getEast() - b.getWest()) * scale; var rh = (b.getNorth() - b.getSouth()) * scale; var mmH = MM_W * h / w; left = Math.max(0, Math.min(left, MM_W - rw)); top = Math.max(0, Math.min(top, mmH - rh)); mmRect.style.left = left + 'px'; mmRect.style.top = top + 'px'; mmRect.style.width = Math.max(4, rw) + 'px'; mmRect.style.height = Math.max(4, rh) + 'px'; } map.on('move zoom', updateMinimap); updateMinimap(); mm.addEventListener('click', function(e) { var rect = mm.getBoundingClientRect(); var scale = MM_W / w; var pixX = (e.clientX - rect.left) / scale; var pixY = (e.clientY - rect.top) / scale; map.flyTo([h - pixY, pixX], map.getZoom(), { animate: true, duration: 0.3 }); }); }()); window.addEventListener('resize', function() { sizeMapSection(); // sizeMapSection already calls invalidateSize + fitCover }); } function fitCover(resetView) { var containerW = map.getContainer().offsetWidth; var containerH = map.getContainer().offsetHeight; var zoomW = Math.log2(containerW / w); var zoomH = Math.log2(containerH / h); var zoom = Math.max(zoomW, zoomH); coverZoom = zoom; map.setMaxZoom(coverZoom + 3.75); map.setMinZoom(coverZoom - 0.75); if (resetView) { var visibleH = containerH / Math.pow(2, zoom); var centerLat = h - visibleH / 2; map.setView([centerLat, w / 2], zoom, { animate: false }); } } function makePinIcon(color) { var fill = color || '#CF1F25'; var isRed = (fill === '#CF1F25'); return L.divIcon({ className: 'fc-pin-icon fc-pin-icon-teardrop' + (isRed ? ' fc-pin-icon-red' : ''), html: '' + '' + '' + '', iconSize: [28, 42], iconAnchor: [14, 42], popupAnchor: [0, -46] }); } function makeSiteIcon() { return L.divIcon({ className: 'fc-pin-icon fc-pin-icon-circle', html: '' + '' + '' + '', iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -18] }); } var _prevView = null; var _prevViewKeepPos = false; var _focusMoveEndFn = null; function focusPin(pin) { _prevViewKeepPos = true; var targetZoom = Math.max((coverZoom || map.getZoom()) + Math.log2(3), coverZoom + 2.25); if (_focusMoveEndFn) { map.off('moveend', _focusMoveEndFn); } _focusMoveEndFn = function() { _focusMoveEndFn = null; showSpotlight(pin); }; map.once('moveend', _focusMoveEndFn); map.flyTo([pin.lat, pin.lng], targetZoom, { animate: true, duration: 0.6 }); } function _flyBackIfNeeded() { _prevView = null; _prevViewKeepPos = false; } function openOverlay(pin) { var overlay = document.getElementById('fc-overlay'); var backdrop = document.getElementById('fc-overlay-backdrop'); overlay.classList.add('is-open'); backdrop.classList.add('is-open'); var kicker = document.getElementById('fc-overlay-category'); var h2 = document.getElementById('fc-overlay-h2'); var content = document.getElementById('fc-overlay-content'); // Set title from JS data; static HTML in #fc-overlay-content stays untouched kicker.textContent = pin.neutral ? 'ZONE' : pin.thematic ? 'ŽIVOT' : 'LOKACIJE'; h2.textContent = pin.title || pin.title_eng; var mapsLink = document.getElementById('fc-overlay-maps-link'); if (!pin.neutral && !pin.thematic && !pin.site && pin.maps_url) { mapsLink.href = pin.maps_url; mapsLink.style.display = 'flex'; } else { mapsLink.style.display = 'none'; mapsLink.href = '#'; } // Reset all dynamic sections so stale/static HTML doesn't show through content.textContent = ''; document.getElementById('fc-overlay-macro-section').style.display = 'none'; document.getElementById('fc-overlay-macro-grid').innerHTML = ''; document.getElementById('fc-overlay-resilience-section').style.display = 'none'; document.getElementById('fc-overlay-resilience-grid').innerHTML = ''; document.getElementById('fc-overlay-gallery-section').style.display = 'none'; document.getElementById('fc-overlay-gallery').innerHTML = ''; document.getElementById('fc-overlay-voh-section').style.display = 'none'; document.getElementById('fc-overlay-voh').innerHTML = ''; document.getElementById('fc-overlay-vg-section').style.display = 'none'; document.getElementById('fc-overlay-vg').innerHTML = ''; if (pin.slug) { fetch('/layout/fcjson-map_bhs-12/map_' + pin.slug) .then(function(r) { return r.text(); }) .then(function(raw) { // EE outputs literal newlines inside text fields which break JSON.parse. // Collapsing all bare newlines to a space makes the JSON valid. var clean = raw.replace(/\r\n|\r|\n/g, ' '); var data; try { data = JSON.parse(clean); } catch (e) { console.error('FC Map JSON parse error:', e.message, '\nRaw (first 500):', raw.slice(0,500)); return; } if (!data) return; // Decode HTML entities produced by EE's |encode modifier function hDec(s) { if (!s) return ''; var t = document.createElement('textarea'); t.innerHTML = s; return t.value; } // Escape URL for use inside HTML attribute strings function escUrl(s) { return String(s || '').replace(/"/g, '%22'); } if (data.title) h2.textContent = hDec(data.title); if (data.desc) content.innerHTML = hDec(data.desc); if (data.maps_url) { mapsLink.href = data.maps_url; mapsLink.style.display = 'flex'; } // ── Macro Stories ────────────────────────────────────────── var macroSection = document.getElementById('fc-overlay-macro-section'); var macroGrid = document.getElementById('fc-overlay-macro-grid'); macroGrid.innerHTML = ''; if (data.macro && data.macro.length) { data.macro.forEach(function(item) { var a = document.createElement('a'); a.href = item.url || '#'; a.target = '_blank'; a.rel = 'noopener'; a.className = 'box cards2'; a.innerHTML = '
' + '
' + item.cat + '
' + '

\u201c' + item.title + '\u201d

'; macroGrid.appendChild(a); }); macroSection.style.display = ''; } else { macroSection.style.display = 'none'; } // ── Resilience ───────────────────────────────────────────── var resSection = document.getElementById('fc-overlay-resilience-section'); var resGrid = document.getElementById('fc-overlay-resilience-grid'); resGrid.innerHTML = ''; if (data.resilience && data.resilience.length) { data.resilience.forEach(function(item) { var a = document.createElement('a'); a.href = hDec(item.url) || '#'; a.target = '_blank'; a.rel = 'noopener'; a.className = 'box cards2'; a.innerHTML = '
' + '

' + hDec(item.title) + '

'; resGrid.appendChild(a); }); resSection.style.display = ''; } else { resSection.style.display = 'none'; } // ── Gallery ──────────────────────────────────────────────── var gallerySection = document.getElementById('fc-overlay-gallery-section'); var galleryGrid = document.getElementById('fc-overlay-gallery'); galleryGrid.innerHTML = ''; if (data.gallery && data.gallery.length) { data.gallery.forEach(function(item) { var a = document.createElement('a'); a.href = '#'; a.className = 'fc-gallery-item'; a.dataset.img = item.src || ''; a.dataset.copyright = item.copy || ''; var img = document.createElement('img'); img.src = item.thumb || item.src || ''; img.alt = hDec(item.alt || ''); img.loading = 'lazy'; a.appendChild(img); galleryGrid.appendChild(a); }); gallerySection.style.display = ''; } else { gallerySection.style.display = 'none'; } // ── VOH ──────────────────────────────────────────────────── var vohSection = document.getElementById('fc-overlay-voh-section'); var vohGrid = document.getElementById('fc-overlay-voh'); vohGrid.innerHTML = ''; if (data.voh && data.voh.length) { data.voh.forEach(function(item) { var div = document.createElement('div'); div.className = 'voh-item'; div.style.cursor = 'pointer'; div.dataset.youtube = item.youtube || ''; // hDec so that textContent display later shows real quotes, not " div.dataset.transcript = hDec(item.transcript || ''); div.innerHTML = '' + '
' + '
' + item.name + '
' + '' + item.headline + '' + '
' + item.id + ' | Video
' + '
'; vohGrid.appendChild(div); }); vohSection.style.display = ''; } else { vohSection.style.display = 'none'; } // ── Video Guide ──────────────────────────────────────────── var vgSection = document.getElementById('fc-overlay-vg-section'); var vgGrid = document.getElementById('fc-overlay-vg'); vgGrid.innerHTML = ''; if (data.vg && data.vg.length) { data.vg.forEach(function(item) { var div = document.createElement('div'); div.className = 'voh-item'; div.style.cursor = 'pointer'; div.dataset.youtube = item.youtube || ''; div.innerHTML = '' + '
' + '
' + item.title + '
' + '' + item.subtitle + '' + '
' + item.title + '
' + '
'; vgGrid.appendChild(div); }); vgSection.style.display = ''; } else { vgSection.style.display = 'none'; } }); } // Save current view so closeOverlay can restore it (only if not coming from list) if (!_prevViewKeepPos) { _prevView = { center: map.getCenter(), zoom: map.getZoom() }; } // On mobile skip pan/zoom — open overlay immediately if (window.matchMedia('(max-width: 768px)').matches) { showSpotlight(pin); return; } // Zoom to slightly above large-tooltip threshold and place pin left or right of overlay var targetZoom = Math.max((coverZoom || map.getZoom()) + Math.log2(3), coverZoom + 2.25); var mapSize = map.getSize(); var pinScreenX = map.latLngToContainerPoint([pin.lat, pin.lng]).x; var overlayHalfW = mapSize.x * 0.44 / 2; var margin = 230; var sideOffset = overlayHalfW + margin; var dirX = (pinScreenX < mapSize.x / 2) ? -sideOffset : sideOffset; var pinProj = map.project([pin.lat, pin.lng], targetZoom); var centreProj = L.point(pinProj.x - dirX, pinProj.y); var adjustedCenter = map.unproject(centreProj, targetZoom); map.flyTo(adjustedCenter, targetZoom, { animate: true, duration: 0.6 }); map.once('moveend', function() {showSpotlight(pin);}); } function closeOverlay() { hideSpotlight(); document.getElementById('fc-overlay').classList.remove('is-open'); document.getElementById('fc-overlay-backdrop').classList.remove('is-open'); _prevView = null; _prevViewKeepPos = false; _spotGroup = null; } function addPin(pin) { // Convert top-left pixel coords (x, y) to Leaflet CRS.Simple (lat = h - y, lng = x) pin.lat = h - pin.y; pin.lng = pin.x; var color = pin.site ? '#1565C0' : (pin.thematic ? '#2F4554' : (pin.neutral ? '#E07B10' : '#CF1F25')); var tooltipClass = pin.site ? 'fc-pin-tooltip-site' : (pin.thematic ? 'fc-pin-tooltip-thematic' : (pin.neutral ? 'fc-pin-tooltip-neutral' : 'fc-pin-tooltip')); var marker = L.marker([pin.lat, pin.lng], { icon: pin.site ? makeSiteIcon() : makePinIcon(color) }).addTo(map); marker.bindTooltip(pin.title || pin.title_eng, { permanent: true, direction: 'top', offset: pin.site ? [0, -10] : [0, -40], className: tooltipClass }); marker.on('tooltipopen', function(e) { var el = e.tooltip.getElement(); if (!el) return; el.addEventListener('click', function(ev) { ev.stopPropagation(); marker.fire('click'); }); }); marker.on('mouseover', function() { if (!document.getElementById('fc-map').classList.contains('fc-map--labels')) { var tt = marker.getTooltip(); if (tt) { var el = tt.getElement(); if (el) el.classList.add('fc-tooltip-hover'); } } }); marker.on('mouseout', function() { var tt = marker.getTooltip(); if (tt) { var el = tt.getElement(); if (el) el.classList.remove('fc-tooltip-hover'); } }); marker.on('click', function() { if (pin.site) { if (pin.url) { if (spotlightPin === pin) { _prevView = null; _prevViewKeepPos = true; hideSpotlight(); } window.open(pin.url, '_blank', 'noopener'); } } else { if (pin._group && pin._group._pins && pin._group._pins.length > 1) { _spotGroup = pin._group._pins; _spotGroupIdx = _spotGroup.indexOf(pin); } else if (!pin.neutral && !pin.thematic && !pin.site) { // red pin — navigate through all red pins _spotGroup = _specificPins; _spotGroupIdx = _specificPins.indexOf(pin); } else { _spotGroup = null; } openOverlay(pin); } }); _allMarkers.push(marker); pin._marker = marker; } document.getElementById('fc-overlay-close').addEventListener('click', closeOverlay); document.getElementById('fc-overlay-backdrop').addEventListener('click', closeOverlay); // ── Location list modals ─────────────────────────────── var locModal = document.getElementById('fc-loc-modal'); var locList = document.getElementById('fc-loc-list'); var _specificPins = SPECIFIC_DATA.slice().sort(function(a, b) { return a.x - b.x; }); SPECIFIC_DATA.slice().sort(function(a, b) { return a.title.localeCompare(b.title, 'bs'); }).forEach(function(pin) { var li = document.createElement('li'); var btn = document.createElement('button'); btn.textContent = pin.title; btn.addEventListener('click', function() { locModal.classList.remove('is-open'); _spotGroup = _specificPins; _spotGroupIdx = _specificPins.indexOf(pin); _prevViewKeepPos = true; openOverlay(pin); }); li.appendChild(btn); locList.appendChild(li); }); var neutralLocModal = document.getElementById('fc-neutral-loc-modal'); var neutralLocList = document.getElementById('fc-neutral-loc-list'); (function() { var sorted = GROUP_DATA.slice().sort(function(a, b) { return a.title.localeCompare(b.title, 'bs'); }); sorted.forEach(function(group) { var li = document.createElement('li'); var btn = document.createElement('button'); btn.textContent = group.title; btn.addEventListener('click', function() { neutralLocModal.classList.remove('is-open'); var pins = group._pins || []; if (!pins.length) return; var idx = Math.floor(Math.random() * pins.length); _spotGroup = pins; _spotGroupIdx = idx; _prevViewKeepPos = true; openOverlay(pins[idx]); }); li.appendChild(btn); neutralLocList.appendChild(li); }); }()); var thematicLocModal = document.getElementById('fc-thematic-loc-modal'); var thematicLocList = document.getElementById('fc-thematic-loc-list'); (function() { var sorted = THEMATIC_DATA.slice().sort(function(a, b) { return a.title.localeCompare(b.title, 'bs'); }); sorted.forEach(function(group) { var li = document.createElement('li'); var btn = document.createElement('button'); btn.textContent = group.title; btn.addEventListener('click', function() { thematicLocModal.classList.remove('is-open'); var pins = group._pins || []; if (!pins.length) return; var idx = Math.floor(Math.random() * pins.length); _spotGroup = pins; _spotGroupIdx = idx; _prevViewKeepPos = true; openOverlay(pins[idx]); }); li.appendChild(btn); thematicLocList.appendChild(li); }); }()); var siteLocModal = document.getElementById('fc-site-loc-modal'); var siteLocList = document.getElementById('fc-site-loc-list'); SITE_DATA.slice().sort(function(a, b) { return (a.title || a.title_eng).localeCompare(b.title || b.title_eng, 'bs', { sensitivity: 'base' }); }).forEach(function(pin) { var li = document.createElement('li'); var btn = document.createElement('button'); btn.textContent = pin.title || pin.title_eng; btn.addEventListener('click', function() { siteLocModal.classList.remove('is-open'); focusPin(pin); }); li.appendChild(btn); siteLocList.appendChild(li); }); function setMarkerVisible(marker, visible) { var el = marker.getElement(); if (!el) return; var icon = el.querySelector('.fc-pin-icon') || el; icon.style.clipPath = visible ? '' : 'polygon(0 0)'; el.style.pointerEvents = visible ? '' : 'none'; } var isMonoMap = false; document.getElementById('fc-mono-toggle').addEventListener('click', function() { isMonoMap = !isMonoMap; mapImageOverlay.setOpacity(isMonoMap ? 0 : 1); mapImageOverlayMono.setOpacity(isMonoMap ? 1 : 0); document.getElementById('fc-mono-icon-mono').style.display = isMonoMap ? 'none' : ''; document.getElementById('fc-mono-icon-color').style.display = isMonoMap ? '' : 'none'; document.getElementById('fc-minimap-img').src = isMonoMap ? 'https://famacollection.org/layout/fc-map-main_thumb.jpg' : 'https://famacollection.org/layout/fc-map-gray_thumb.jpg'; this.title = isMonoMap ? 'Colour map' : 'Black & white map'; btnAnim('fc-mono-toggle'); }); var pinsVisible = true; document.getElementById('fc-pins-toggle').addEventListener('click', function() { pinsVisible = !pinsVisible; document.getElementById('fc-map').classList.toggle('fc-pins-specific-hidden', !pinsVisible); SPECIFIC_DATA.forEach(function(pin) { if (pin._marker) { setMarkerVisible(pin._marker, pinsVisible); } }); this.classList.toggle('pins-hidden', !pinsVisible); this.title = pinsVisible ? 'Show/hide locations' : 'Show locations'; btnAnim('fc-pins-toggle'); var locBtn = document.getElementById('fc-loc-btn'); locBtn.style.display = pinsVisible ? '' : 'none'; if (!pinsVisible) locModal.classList.remove('is-open'); }); var neutralVisible = true; document.getElementById('fc-neutral-toggle').addEventListener('click', function() { neutralVisible = !neutralVisible; var fcMap = document.getElementById('fc-map'); fcMap.classList.toggle('fc-pins-neutral-hidden', !neutralVisible); GROUP_DATA.forEach(function(group) { (group._pins || []).forEach(function(pin) { if (!pin._marker) return; if (neutralVisible) {pin._marker.addTo(map);} else {pin._marker.remove();} }); }); this.classList.toggle('pins-hidden', !neutralVisible); this.title = neutralVisible ? 'Show/hide groups' : 'Show groups'; btnAnim('fc-neutral-toggle'); var neutralLocBtn = document.getElementById('fc-neutral-loc-btn'); neutralLocBtn.style.display = neutralVisible ? '' : 'none'; if (!neutralVisible) neutralLocModal.classList.remove('is-open'); }); var thematicVisible = true; document.getElementById('fc-thematic-toggle').addEventListener('click', function() { thematicVisible = !thematicVisible; var fcMap = document.getElementById('fc-map'); fcMap.classList.toggle('fc-pins-thematic-hidden', !thematicVisible); THEMATIC_DATA.forEach(function(group) { (group._pins || []).forEach(function(pin) { if (!pin._marker) return; if (thematicVisible) {pin._marker.addTo(map);} else {pin._marker.remove();} }); }); this.classList.toggle('pins-hidden', !thematicVisible); this.title = thematicVisible ? 'Show/hide thematic' : 'Show thematic'; btnAnim('fc-thematic-toggle'); var thematicLocBtn = document.getElementById('fc-thematic-loc-btn'); thematicLocBtn.style.display = thematicVisible ? '' : 'none'; if (!thematicVisible) thematicLocModal.classList.remove('is-open'); }); var siteVisible = true; document.getElementById('fc-site-toggle').addEventListener('click', function() { siteVisible = !siteVisible; document.getElementById('fc-map').classList.toggle('fc-pins-site-hidden', !siteVisible); SITE_DATA.forEach(function(pin) { if (!pin._marker) return; if (siteVisible) {pin._marker.addTo(map);} else {pin._marker.remove();} }); this.classList.toggle('pins-hidden', !siteVisible); this.title = siteVisible ? 'Show/hide site locations' : 'Show site locations'; btnAnim('fc-site-toggle'); var siteLocBtn = document.getElementById('fc-site-loc-btn'); siteLocBtn.style.display = siteVisible ? '' : 'none'; if (!siteVisible) siteLocModal.classList.remove('is-open'); }); document.getElementById('fc-loc-btn').addEventListener('click', function(e) { e.stopPropagation(); btnAnim('fc-loc-btn'); neutralLocModal.classList.remove('is-open'); thematicLocModal.classList.remove('is-open'); siteLocModal.classList.remove('is-open'); locModal.classList.toggle('is-open'); }); document.getElementById('fc-neutral-loc-btn').addEventListener('click', function(e) { e.stopPropagation(); btnAnim('fc-neutral-loc-btn'); locModal.classList.remove('is-open'); thematicLocModal.classList.remove('is-open'); siteLocModal.classList.remove('is-open'); neutralLocModal.classList.toggle('is-open'); }); document.getElementById('fc-thematic-loc-btn').addEventListener('click', function(e) { e.stopPropagation(); btnAnim('fc-thematic-loc-btn'); locModal.classList.remove('is-open'); neutralLocModal.classList.remove('is-open'); siteLocModal.classList.remove('is-open'); thematicLocModal.classList.toggle('is-open'); }); document.getElementById('fc-site-loc-btn').addEventListener('click', function(e) { e.stopPropagation(); btnAnim('fc-site-loc-btn'); locModal.classList.remove('is-open'); neutralLocModal.classList.remove('is-open'); thematicLocModal.classList.remove('is-open'); siteLocModal.classList.toggle('is-open'); }); document.addEventListener('click', function(e) { if (!e.target.closest('#fc-loc-modal') && !e.target.closest('#fc-loc-btn')) {locModal.classList.remove('is-open');} if (!e.target.closest('#fc-neutral-loc-modal') && !e.target.closest('#fc-neutral-loc-btn')) {neutralLocModal.classList.remove('is-open');} if (!e.target.closest('#fc-thematic-loc-modal') && !e.target.closest('#fc-thematic-loc-btn')) {thematicLocModal.classList.remove('is-open');} if (!e.target.closest('#fc-site-loc-modal') && !e.target.closest('#fc-site-loc-btn')) {siteLocModal.classList.remove('is-open');} }); // ── Spotlight ──────────────────────────────── var spotlightPin = null; var spotlightEl = document.getElementById('fc-spotlight'); var spotCircle = document.getElementById('spot-circle'); var spotBg = document.getElementById('spot-bg'); var SPOT_R = 200; var _spotGroup = null; var _spotGroupIdx = 0; var spotPrevBtn = document.getElementById('fc-spot-prev'); var spotNextBtn = document.getElementById('fc-spot-next'); function _updateSpotNav() { var show = _spotGroup && _spotGroup.length > 1; spotPrevBtn.classList.toggle('is-visible', show); spotNextBtn.classList.toggle('is-visible', show); if (show) { spotPrevBtn.disabled = false; spotNextBtn.disabled = false; } } function navigateSpot(dir) { if (!_spotGroup) return; _spotGroupIdx = (_spotGroupIdx + dir + _spotGroup.length) % _spotGroup.length; var pin = _spotGroup[_spotGroupIdx]; _prevViewKeepPos = true; openOverlay(pin); } spotPrevBtn.addEventListener('click', function() {navigateSpot(-1);}); spotNextBtn.addEventListener('click', function() {navigateSpot(1);}); function updateSpotlight() { if (!spotlightPin) return; var pt = map.latLngToContainerPoint([spotlightPin.lat, spotlightPin.lng]); spotCircle.setAttribute('cx', pt.x); spotCircle.setAttribute('cy', pt.y + 10); } var _spotForcedColor = false; function showSpotlight(pin) { spotlightPin = pin; updateSpotlight(); spotlightEl.classList.add('is-active'); map.off('move', updateSpotlight); // guard against duplicate listeners map.on('move', updateSpotlight); _updateSpotNav(); // hide info panel var infoPanel = document.getElementById('fc-info-panel'); if (infoPanel) infoPanel.style.display = 'none'; // if map is mono, temporarily switch to color if (isMonoMap) { _spotForcedColor = true; mapImageOverlay.setOpacity(1); mapImageOverlayMono.setOpacity(0); } } function hideSpotlight() { spotlightPin = null; spotCircle.setAttribute('cx', -9999); spotlightEl.classList.remove('is-active'); spotPrevBtn.classList.remove('is-visible'); spotNextBtn.classList.remove('is-visible'); map.off('move', updateSpotlight); // restore info panel var infoPanel = document.getElementById('fc-info-panel'); if (infoPanel) infoPanel.style.display = ''; // revert to mono if we forced color if (_spotForcedColor) { _spotForcedColor = false; mapImageOverlay.setOpacity(0); mapImageOverlayMono.setOpacity(1); } } spotBg.addEventListener('click', function() { var overlayOpen = document.getElementById('fc-overlay').classList.contains('is-open'); hideSpotlight(); if (!overlayOpen) {_flyBackIfNeeded();} }); document.getElementById('fc-reset').addEventListener('click', hideSpotlight); // Close spotlight on click outside circle — pointerup fires even after drag var _pdX, _pdY; var _blockSpotlight = false; document.addEventListener('pointerdown', function(e) { _pdX = e.clientX; _pdY = e.clientY; }); document.addEventListener('pointerup', function(e) { if (!spotlightPin) return; // spotlight is tied to the overlay — let the overlay backdrop handle dismissal if (document.getElementById('fc-overlay').classList.contains('is-open')) return; var dd = Math.sqrt(Math.pow(e.clientX - _pdX, 2) + Math.pow(e.clientY - _pdY, 2)); if (dd > 6) return; // was a drag var rect = map.getContainer().getBoundingClientRect(); if (e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom) return; // outside map var cx = e.clientX - rect.left; var cy = e.clientY - rect.top; var pt = map.latLngToContainerPoint([spotlightPin.lat, spotlightPin.lng]); var dx = cx - pt.x, dy = cy - pt.y; if (Math.sqrt(dx * dx + dy * dy) > SPOT_R) { hideSpotlight(); _flyBackIfNeeded(); } }); function blockSpotlight() { _blockSpotlight = true; } function unblockSpotlight() { setTimeout(function() { _blockSpotlight = false; }, 100); } // ── Lightbox ──────────────────────────────────────────── var lightbox = document.getElementById('fc-lightbox'); var lightboxImg = document.getElementById('fc-lightbox-img'); var lightboxCopy = document.getElementById('fc-lightbox-copy'); var lbPrev = document.getElementById('fc-lightbox-prev'); var lbNext = document.getElementById('fc-lightbox-next'); var lbItems = []; var lbIndex = 0; function lbShow(index) { lbIndex = index; var item = lbItems[lbIndex]; lightboxImg.src = item.dataset.img; lightboxCopy.textContent = item.dataset.copyright || ''; lbPrev.disabled = false; lbNext.disabled = false; } function openLightbox(item) { blockSpotlight(); lbItems = Array.from(document.querySelectorAll('.fc-gallery-item')); lbShow(lbItems.indexOf(item)); lightbox.classList.add('is-open'); } function closeLightbox() { lightbox.classList.remove('is-open'); lightboxImg.src = ''; unblockSpotlight(); if (spotlightPin && spotlightPin._marker) spotlightPin._marker.openTooltip(); } document.getElementById('fc-lightbox-close').addEventListener('click', closeLightbox); lbPrev.addEventListener('click', function(e) { e.stopPropagation(); if (lbIndex > 0) lbShow(lbIndex - 1); }); lbNext.addEventListener('click', function(e) { e.stopPropagation(); if (lbIndex < lbItems.length - 1) lbShow(lbIndex + 1); }); lightbox.addEventListener('click', function(e) { if (e.target === lightbox || e.target === document.getElementById('fc-lightbox-inner')) closeLightbox(); }); // ── Lightbox touch swipe ───────────────────────────── (function() { var _lbTouchX = null; lightbox.addEventListener('touchstart', function(e) { _lbTouchX = e.touches[0].clientX; }, { passive: true }); lightbox.addEventListener('touchend', function(e) { if (_lbTouchX === null) return; var dx = e.changedTouches[0].clientX - _lbTouchX; _lbTouchX = null; if (Math.abs(dx) < 40) return; if (dx < 0 && lbIndex < lbItems.length - 1) lbShow(lbIndex + 1); else if (dx > 0 && lbIndex > 0) lbShow(lbIndex - 1); }, { passive: true }); })(); document.addEventListener('click', function(e) { var item = e.target.closest('.fc-gallery-item'); if (!item) return; e.preventDefault(); openLightbox(item); }); // ── VOH Modal ───────────────────────────────────────── (function() { var modal = document.getElementById('voh-modal'); var overlay = document.getElementById('voh-modal-overlay'); var closeBtn = document.getElementById('voh-modal-close'); var iframe = document.getElementById('voh-modal-iframe'); var labelEl = document.getElementById('voh-modal-label'); var idEl = document.getElementById('voh-modal-id'); var titleEl = document.getElementById('voh-modal-title'); var transcriptEl = document.getElementById('voh-modal-transcript-text'); var prevBtn = document.getElementById('voh-modal-prev'); var nextBtn = document.getElementById('voh-modal-next'); var counterEl = document.getElementById('voh-modal-nav-counter'); var currentIndex = 0; var _vgMode = false; function getActiveItems() { return Array.from(document.querySelectorAll(_vgMode ? '#fc-overlay-vg .voh-item' : '#fc-overlay-voh .voh-item')); } function renderModal(item) { var activeItems = getActiveItems(); var youtubeId = item.dataset.youtube || ''; var transcript = item.dataset.transcript || ''; var labelNode = item.querySelector('.voh-label'); var titleNode = item.querySelector('.voh-title'); var metaNode = item.querySelector('.voh-meta'); labelEl.textContent = labelNode ? labelNode.textContent : ''; titleEl.textContent = titleNode ? titleNode.textContent : ''; transcriptEl.textContent = transcript; idEl.textContent = metaNode ? metaNode.textContent.split('|')[0].trim() : ''; iframe.src = youtubeId ? 'https://www.youtube-nocookie.com/embed/' + youtubeId + '?autoplay=1&rel=0' : ''; // VG mode: hide label, suppress nav when only one item var navEl = document.querySelector('.voh-modal-nav'); labelEl.style.display = _vgMode ? 'none' : ''; modal.classList.toggle('vg-mode', _vgMode); if (navEl) navEl.style.display = (_vgMode && activeItems.length <= 1) ? 'none' : ''; counterEl.textContent = (currentIndex + 1) + ' / ' + activeItems.length; prevBtn.disabled = false; nextBtn.disabled = false; } function openVohModal(item) { blockSpotlight(); currentIndex = getActiveItems().indexOf(item); renderModal(item); modal.classList.add('active'); document.body.style.overflow = 'hidden'; } function closeVohModal() { modal.classList.remove('active'); iframe.src = ''; document.body.style.overflow = ''; unblockSpotlight(); if (spotlightPin && spotlightPin._marker) spotlightPin._marker.openTooltip(); } // Event delegation — works for both static and dynamically-added voh-items document.addEventListener('click', function(e) { var trigger = e.target.closest('.voh-thumb, .voh-title, .voh-type'); if (!trigger) return; var item = trigger.closest('.voh-item'); if (!item) return; e.preventDefault(); _vgMode = !!item.closest('#fc-overlay-vg'); openVohModal(item); }); prevBtn.addEventListener('click', function() { var activeItems = getActiveItems(); currentIndex = (currentIndex - 1 + activeItems.length) % activeItems.length; renderModal(activeItems[currentIndex]); modal.scrollTop = 0; }); nextBtn.addEventListener('click', function() { var activeItems = getActiveItems(); currentIndex = (currentIndex + 1) % activeItems.length; renderModal(activeItems[currentIndex]); modal.scrollTop = 0; }); overlay.addEventListener('click', closeVohModal); closeBtn.addEventListener('click', closeVohModal); document.addEventListener('keydown', function(e) { if (!modal.classList.contains('active')) return; var activeItems = getActiveItems(); if (e.key === 'Escape') { e.stopImmediatePropagation(); closeVohModal(); return; } if (e.key === 'ArrowLeft') { currentIndex = (currentIndex - 1 + activeItems.length) % activeItems.length; renderModal(activeItems[currentIndex]); modal.scrollTop = 0; } if (e.key === 'ArrowRight') { currentIndex = (currentIndex + 1) % activeItems.length; renderModal(activeItems[currentIndex]); modal.scrollTop = 0; } }); }()); document.addEventListener('keydown', function(e) { if (!lightbox.classList.contains('is-open')) return; if (e.key === 'Escape') { e.stopImmediatePropagation(); closeLightbox(); return; } if (e.key === 'ArrowLeft') { e.preventDefault(); lbShow((lbIndex - 1 + lbItems.length) % lbItems.length); } if (e.key === 'ArrowRight') { e.preventDefault(); lbShow((lbIndex + 1) % lbItems.length); } }); // ── Keyboard spotlight group navigation ─────────────── document.addEventListener('keydown', function(e) { if (!document.getElementById('fc-overlay').classList.contains('is-open')) return; if (!_spotGroup || _spotGroup.length <= 1) return; if (document.getElementById('fc-lightbox').classList.contains('is-open')) return; if (document.getElementById('voh-modal').classList.contains('active')) return; if (e.key === 'ArrowLeft') { navigateSpot(-1); e.preventDefault(); } if (e.key === 'ArrowRight') { navigateSpot(1); e.preventDefault(); } }); // ── Keyboard map navigation ──────────────────────────── document.addEventListener('keydown', function(e) { // Skip if focus is on an input/textarea if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; // Escape always closes whatever is open — layered: sub-modals first, overlay second if (e.key === 'Escape') { if (lightbox.classList.contains('is-open')) return; if (document.getElementById('voh-modal').classList.contains('active')) return; closeOverlay(); hideSpotlight(); _flyBackIfNeeded(); locModal.classList.remove('is-open'); neutralLocModal.classList.remove('is-open'); thematicLocModal.classList.remove('is-open'); siteLocModal.classList.remove('is-open'); return; } // Skip map pan/zoom if any modal is open if (document.getElementById('fc-overlay').classList.contains('is-open')) return; if (document.getElementById('fc-lightbox').classList.contains('is-open')) return; if (document.getElementById('voh-modal').classList.contains('active')) return; var PAN = 120; switch (e.key) { case 'ArrowLeft': map.panBy([-PAN, 0], { animate: true }); e.preventDefault(); break; case 'ArrowRight': map.panBy([PAN, 0], { animate: true }); e.preventDefault(); break; case 'ArrowUp': map.panBy([0, -PAN], { animate: true }); e.preventDefault(); break; case 'ArrowDown': map.panBy([0, PAN], { animate: true }); e.preventDefault(); break; case '+': case '=': map.flyTo(map.getCenter(), Math.min(map.getZoom() + 0.75, coverZoom + 3.75), { animate: true, duration: 0.35 }); break; case '-': map.flyTo(map.getCenter(), Math.max(map.getZoom() - 0.75, coverZoom - 0.75), { animate: true, duration: 0.35 }); break; case '0': document.getElementById('fc-reset').click(); break; } }); }());