Skip to content

Instantly share code, notes, and snippets.

@redraw
Last active August 1, 2026 22:09
Show Gist options
  • Select an option

  • Save redraw/3b9c120cb121290c11d9daa125a2828f to your computer and use it in GitHub Desktop.

Select an option

Save redraw/3b9c120cb121290c11d9daa125a2828f to your computer and use it in GitHub Desktop.
IMAX map
<meta charset="utf-8" />
<title>IMAX Atlas — Global Screen Directory</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
:root {
--bg: #0b0d12;
--panel: #14171f;
--panel-2: #191d27;
--land: #1e2330;
--land-line: #2b3142;
--ocean: #0e1017;
--hair: #2a2f3c;
--text-hi: #ece9e2;
--text-lo: #90959f;
--text-faint: #565b68;
--marquee: #f2a93b;
--marquee-dim: #6b5326;
--reel: #4fd1c5;
--dot-standard: #545b6b;
--focus: #f2a93b;
--shadow: 0 20px 50px -20px rgba(0, 0, 0, 0.6);
}
:root[data-theme="light"] {
--bg: #eceef2;
--panel: #ffffff;
--panel-2: #f4f5f8;
--land: #d7dae3;
--land-line: #b7bbc8;
--ocean: #e4e7ee;
--hair: #d6d9e0;
--text-hi: #191b22;
--text-lo: #565c6b;
--text-faint: #9aa0ad;
--marquee: #b8720f;
--marquee-dim: #e8cfa1;
--reel: #16847a;
--dot-standard: #a6abb8;
--focus: #b8720f;
--shadow: 0 20px 50px -20px rgba(30, 34, 48, 0.25);
}
@media (prefers-color-scheme: light) {
:root:not([data-theme="dark"]) {
--bg: #eceef2;
--panel: #ffffff;
--panel-2: #f4f5f8;
--land: #d7dae3;
--land-line: #b7bbc8;
--ocean: #e4e7ee;
--hair: #d6d9e0;
--text-hi: #191b22;
--text-lo: #565c6b;
--text-faint: #9aa0ad;
--marquee: #b8720f;
--marquee-dim: #e8cfa1;
--reel: #16847a;
--dot-standard: #a6abb8;
--focus: #b8720f;
--shadow: 0 20px 50px -20px rgba(30, 34, 48, 0.25);
}
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: var(--bg);
color: var(--text-hi);
font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.label {
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.11em;
font-size: 11px;
color: var(--text-lo);
}
.num { font-variant-numeric: tabular-nums; }
.app { position: relative; width: 100vw; height: 100vh; isolation: isolate; }
#map {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
background: var(--ocean);
z-index: 0;
}
#map .leaflet-control-attribution {
background: var(--panel);
color: var(--text-faint);
border-top-left-radius: 6px;
}
#map .leaflet-control-attribution a { color: var(--text-lo); }
.dot {
fill: var(--dot-standard);
stroke: var(--ocean);
stroke-width: 1;
cursor: pointer;
}
.dot.laser { fill: var(--marquee); }
.dot.dome { fill: var(--reel); }
.dot.f70 { stroke: var(--reel); stroke-width: 2; }
.dot.dim { opacity: 0.08; pointer-events: none; }
.dot.active { stroke: var(--text-hi); stroke-width: 2; }
/* ---------- Control deck ---------- */
.deck {
position: absolute;
top: 18px;
left: 18px;
width: 320px;
max-height: calc(100vh - 36px);
background: var(--panel);
border: 1px solid var(--hair);
border-radius: 10px;
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
overflow: hidden;
z-index: 1100;
}
.deck-head {
padding: 16px 18px 14px;
border-bottom: 1px solid var(--hair);
flex-shrink: 0;
position: relative;
padding-right: 52px;
}
.deck.collapsed .deck-head { border-bottom: none; }
.deck-toggle {
position: absolute;
top: 50%;
right: 12px;
width: 30px;
height: 30px;
transform: translateY(-50%);
border: 1px solid var(--hair);
border-radius: 6px;
background: var(--panel-2);
color: var(--text-hi);
cursor: pointer;
font-size: 17px;
line-height: 1;
}
.deck-toggle:hover { border-color: var(--text-lo); }
.deck-toggle:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.deck.collapsed .deck-body { display: none; }
.brand {
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.14em;
font-size: 15px;
color: var(--text-hi);
display: flex;
align-items: baseline;
gap: 8px;
}
.brand .dot-mark {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--marquee);
display: inline-block;
margin-bottom: 1px;
}
.brand-sub {
margin-top: 3px;
font-size: 12px;
color: var(--text-lo);
}
.deck-body {
padding: 14px 18px 18px;
overflow-y: auto;
flex: 1;
}
.deck-body::-webkit-scrollbar { width: 6px; }
.deck-body::-webkit-scrollbar-thumb { background: var(--hair); border-radius: 3px; }
.group { margin-bottom: 18px; }
.group:last-child { margin-bottom: 0; }
.group-title {
margin: 0 0 8px;
}
.search-wrap { position: relative; }
input[type="text"] {
width: 100%;
background: var(--panel-2);
border: 1px solid var(--hair);
color: var(--text-hi);
border-radius: 7px;
padding: 9px 11px;
font-size: 13.5px;
outline: none;
font-family: inherit;
}
input[type="text"]:focus { border-color: var(--focus); }
input[type="text"]::placeholder { color: var(--text-faint); }
.suggestions {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
background: var(--panel-2);
border: 1px solid var(--hair);
border-radius: 7px;
box-shadow: var(--shadow);
max-height: 220px;
overflow-y: auto;
z-index: 30;
display: none;
}
.suggestions.open { display: block; }
.sugg-item {
padding: 8px 11px;
cursor: pointer;
border-bottom: 1px solid var(--hair);
}
.sugg-item:last-child { border-bottom: none; }
.sugg-item:hover, .sugg-item.hi { background: var(--panel); }
.sugg-name { font-size: 13px; color: var(--text-hi); }
.sugg-meta { font-size: 11px; color: var(--text-lo); margin-top: 1px; }
.chip-row { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
border: 1px solid var(--hair);
background: var(--panel-2);
color: var(--text-lo);
border-radius: 999px;
padding: 6px 11px;
font-size: 12px;
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
gap: 6px;
transition: border-color 0.12s, color 0.12s;
}
.chip .n { font-variant-numeric: tabular-nums; color: var(--text-faint); }
.chip:hover { border-color: var(--text-lo); }
.chip.on {
background: var(--marquee-dim);
border-color: var(--marquee);
color: var(--text-hi);
}
.chip.on .n { color: var(--text-hi); opacity: 0.7; }
.fmt-row { display: flex; flex-direction: column; gap: 7px; }
.fmt-item {
display: flex;
align-items: center;
gap: 9px;
padding: 7px 9px;
border-radius: 7px;
cursor: pointer;
border: 1px solid transparent;
}
.fmt-item:hover { background: var(--panel-2); }
.fmt-item.on { border-color: var(--hair); background: var(--panel-2); }
.swatch { width: 11px; height: 11px; border-radius: 50%; flex-shrink: 0; }
.swatch.laser { background: var(--marquee); }
.swatch.dome { background: var(--reel); }
.swatch.f70 { background: var(--dot-standard); box-shadow: inset 0 0 0 1.6px var(--reel); }
.fmt-text { font-size: 13px; color: var(--text-hi); flex: 1; }
.fmt-count { font-size: 11px; color: var(--text-faint); font-variant-numeric: tabular-nums; }
select {
width: 100%;
background: var(--panel-2);
border: 1px solid var(--hair);
color: var(--text-hi);
border-radius: 7px;
padding: 9px 11px;
font-size: 13px;
outline: none;
font-family: inherit;
}
select:focus { border-color: var(--focus); }
.clear-btn {
width: 100%;
margin-top: 4px;
background: transparent;
border: 1px solid var(--hair);
color: var(--text-lo);
border-radius: 7px;
padding: 8px 0;
font-size: 12px;
letter-spacing: 0.05em;
text-transform: uppercase;
font-weight: 700;
cursor: pointer;
}
.clear-btn:hover { color: var(--text-hi); border-color: var(--text-lo); }
/* ---------- Marquee readout ---------- */
.marquee-box {
position: absolute;
left: 18px;
bottom: 18px;
background: var(--panel);
border: 1px solid var(--hair);
border-radius: 10px;
box-shadow: var(--shadow);
padding: 12px 16px;
z-index: 1100;
min-width: 200px;
}
.marquee-count {
font-size: 28px;
font-weight: 800;
letter-spacing: 0.01em;
color: var(--marquee);
line-height: 1;
font-variant-numeric: tabular-nums;
}
.marquee-total {
font-size: 12px;
color: var(--text-lo);
margin-top: 4px;
}
/* ---------- Zoom controls ---------- */
.zoom-ctl {
position: absolute;
right: 18px;
bottom: 18px;
display: flex;
flex-direction: column;
background: var(--panel);
border: 1px solid var(--hair);
border-radius: 10px;
box-shadow: var(--shadow);
overflow: hidden;
z-index: 1100;
}
.zoom-btn {
width: 38px;
height: 38px;
border: none;
background: transparent;
color: var(--text-hi);
font-size: 18px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.zoom-btn:hover { background: var(--panel-2); }
.zoom-btn:not(:last-child) { border-bottom: 1px solid var(--hair); }
/* ---------- Popover ---------- */
.popover {
position: absolute;
width: 250px;
background: var(--panel);
border: 1px solid var(--hair);
border-radius: 10px;
box-shadow: var(--shadow);
padding: 14px 15px;
z-index: 1200;
display: none;
}
.popover.open { display: block; }
.pop-close {
position: absolute;
top: 8px;
right: 10px;
background: none;
border: none;
color: var(--text-faint);
font-size: 15px;
cursor: pointer;
line-height: 1;
}
.pop-close:hover { color: var(--text-hi); }
.pop-name {
font-size: 14.5px;
font-weight: 700;
color: var(--text-hi);
padding-right: 16px;
margin-bottom: 3px;
}
.pop-loc { font-size: 12.5px; color: var(--text-lo); margin-bottom: 10px; }
.pop-badges { display: flex; flex-wrap: wrap; gap: 5px; }
.badge {
font-size: 10.5px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 3px 7px;
border-radius: 5px;
background: var(--panel-2);
color: var(--text-lo);
border: 1px solid var(--hair);
}
.badge.laser { color: var(--marquee); border-color: var(--marquee-dim); }
.badge.dome, .badge.f70 { color: var(--reel); border-color: var(--reel); }
@media (max-width: 640px) {
.deck { width: calc(100vw - 24px); max-height: 46vh; }
.marquee-box { left: 12px; bottom: 12px; padding: 9px 12px; }
.marquee-count { font-size: 22px; }
}
@media (prefers-reduced-motion: reduce) {
* { transition: none !important; }
}
/* ---------- Loading / error overlay ---------- */
.loading-overlay {
position: absolute;
inset: 0;
z-index: 1300;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg);
transition: opacity 0.25s ease;
}
.loading-overlay.hidden { opacity: 0; pointer-events: none; }
.loading-box { display: flex; flex-direction: column; align-items: center; gap: 10px; max-width: 320px; text-align: center; padding: 0 20px; }
.loading-spinner {
width: 30px;
height: 30px;
border-radius: 50%;
border: 2.5px solid var(--hair);
border-top-color: var(--marquee);
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
.loading-spinner { animation: none; border-top-color: var(--hair); }
}
.loading-text { font-size: 13.5px; color: var(--text-hi); }
.loading-sub { font-size: 12px; color: var(--text-lo); }
.retry-btn {
margin-top: 6px;
background: var(--panel-2);
border: 1px solid var(--hair);
color: var(--text-hi);
border-radius: 7px;
padding: 7px 14px;
font-size: 12.5px;
cursor: pointer;
}
.retry-btn:hover { border-color: var(--text-lo); }
.source-note { font-size: 11px; color: var(--text-faint); margin-top: 10px; text-align: center; }
.source-note a { color: var(--text-lo); text-decoration: none; border-bottom: 1px dotted var(--hair); }
.source-note a:hover { color: var(--text-hi); }
</style>
<div class="app">
<div id="map"></div>
<div class="deck" id="deck">
<div class="deck-head">
<div class="brand"><span class="dot-mark"></span>IMAX Atlas</div>
<div class="brand-sub">Global screen directory — <span class="num" id="totalCount">0</span> venues</div>
<button class="deck-toggle" id="deckToggle" type="button" aria-expanded="true" aria-controls="deckBody" title="Collapse filters">−</button>
</div>
<div class="deck-body" id="deckBody">
<div class="group">
<div class="search-wrap">
<input type="text" id="searchInput" placeholder="Search theatre, city or country…" autocomplete="off" />
<div class="suggestions" id="suggestions"></div>
</div>
</div>
<div class="group">
<div class="label group-title">Region</div>
<div class="chip-row" id="regionChips"></div>
</div>
<div class="group">
<div class="label group-title">Country</div>
<select id="countrySelect"></select>
</div>
<div class="group">
<div class="label group-title">Screen format</div>
<div class="fmt-row" id="formatRow"></div>
</div>
<button class="clear-btn" id="clearBtn">Clear all filters</button>
<div class="source-note">Live from <a href="https://github.com/sMkr8geh/imax_global_database" target="_blank" rel="noopener">imax_global_database</a></div>
</div>
</div>
<div class="marquee-box">
<div class="marquee-count num" id="shownCount">0</div>
<div class="marquee-total">of <span class="num" id="totalCount2">0</span> IMAX theatres shown</div>
</div>
<div class="zoom-ctl">
<button class="zoom-btn" id="zoomIn" title="Zoom in">+</button>
<button class="zoom-btn" id="zoomOut" title="Zoom out">&minus;</button>
<button class="zoom-btn" id="zoomReset" title="Reset view" style="font-size:13px;">⤢</button>
</div>
<div class="popover" id="popover">
<button class="pop-close" id="popClose">&times;</button>
<div class="pop-name" id="popName"></div>
<div class="pop-loc" id="popLoc"></div>
<div class="pop-badges" id="popBadges"></div>
</div>
<div class="loading-overlay" id="loadingOverlay">
<div class="loading-box">
<div class="loading-spinner" id="loadingSpinner"></div>
<div class="loading-text" id="loadingText">Fetching live theatre data…</div>
<div class="loading-sub" id="loadingSub">imax_global_database (GitHub)</div>
<button class="retry-btn" id="retryBtn" style="display:none;">Retry</button>
</div>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
(function () {
"use strict";
var CSV_URL = "https://raw.githubusercontent.com/sMkr8geh/imax_global_database/refs/heads/main/imax_global.csv";
// Keep the map usable when the live GitHub source is blocked (for example by
// an offline preview or a restrictive network). The live dataset replaces
// this on normal loads.
var FALLBACK_DATA = [
["TCL Chinese Theatre IMAX", "Los Angeles", "CA", "United States", "NA", 34.1022, -118.3407, 1, 0, 0, null],
["AMC Lincoln Square 13 IMAX", "New York", "NY", "United States", "NA", 40.7750, -73.9830, 1, 0, 1, null],
["BFI IMAX", "London", null, "United Kingdom", "EMEA", 51.5020, -0.1134, 1, 0, 0, null],
["Cineplex Scotiabank Theatre Toronto", "Toronto", "ON", "Canada", "NA", 43.6487, -79.3917, 1, 0, 0, null],
["IMAX Melbourne", "Melbourne", "VIC", "Australia", "APAC", -37.8034, 144.9719, 1, 0, 1, null],
["TGV Sunway Velocity IMAX", "Kuala Lumpur", null, "Malaysia", "APAC", 3.1277, 101.7264, 1, 0, 0, null],
["Cinepolis IMAX Perisur", "Mexico City", null, "Mexico", "LATAM", 19.3038, -99.1883, 1, 0, 0, null],
["IMAX Sydney", "Sydney", "NSW", "Australia", "APAC", -33.8592, 151.2093, 1, 0, 0, null]
];
var NAME_FIX = {
"United States of America": "United States",
"Korea, Republic of": "South Korea",
"Russian Federation": "Russia",
"Taiwan, Province of China": "Taiwan",
"Viet Nam": "Vietnam",
"United Kingdom of Great Britain and Northern Ireland": "United Kingdom"
};
function parseCSV(text) {
if (text.charCodeAt(0) === 0xfeff) text = text.slice(1);
var rows = [], row = [], field = "", inQuotes = false;
for (var i = 0; i < text.length; i++) {
var c = text[i];
if (inQuotes) {
if (c === '"') {
if (text[i + 1] === '"') { field += '"'; i++; }
else { inQuotes = false; }
} else {
field += c;
}
} else if (c === '"') {
inQuotes = true;
} else if (c === ",") {
row.push(field); field = "";
} else if (c === "\r") {
// skip
} else if (c === "\n") {
row.push(field); rows.push(row); row = []; field = "";
} else {
field += c;
}
}
if (field.length || row.length) { row.push(field); rows.push(row); }
return rows;
}
// row: [name, city, state, countryName, region, lat, lng, laser, dome, f70, operator]
function normalizeRows(table) {
if (!table.length) return [];
var idx = {};
table[0].forEach(function (h, i) { idx[h.trim()] = i; });
var out = [];
for (var r = 1; r < table.length; r++) {
var row = table[r];
if (!row || row.length < 2) continue;
var name = (row[idx["name"]] || "").trim();
if (!name) continue;
var lat = parseFloat(row[idx["_geoloc.lat"]]);
var lng = parseFloat(row[idx["_geoloc.lng"]]);
if (isNaN(lat) || isNaN(lng)) continue;
var countryName = (row[idx["countryName"]] || "").trim();
countryName = NAME_FIX[countryName] || countryName;
var region = (row[idx["region"]] || "").trim();
var isTrue = function (v) { return /^true$/i.test((v || "").trim()); };
out.push([
name,
(row[idx["city"]] || "").trim() || null,
(row[idx["state"]] || "").trim() || null,
countryName,
region,
lat,
lng,
isTrue(row[idx["Laser"]]) ? 1 : 0,
isTrue(row[idx["Dome"]]) ? 1 : 0,
isTrue(row[idx["70mm"]]) ? 1 : 0,
(row[idx["operator"]] || "").trim() || null
]);
}
return out;
}
var loadingOverlay = document.getElementById("loadingOverlay");
var loadingText = document.getElementById("loadingText");
var loadingSub = document.getElementById("loadingSub");
var loadingSpinner = document.getElementById("loadingSpinner");
var retryBtn = document.getElementById("retryBtn");
var deck = document.getElementById("deck");
var deckToggle = document.getElementById("deckToggle");
function setDeckCollapsed(collapsed) {
deck.classList.toggle("collapsed", collapsed);
deckToggle.textContent = collapsed ? "+" : "−";
deckToggle.setAttribute("aria-expanded", String(!collapsed));
deckToggle.title = collapsed ? "Expand filters" : "Collapse filters";
}
deckToggle.addEventListener("click", function () {
setDeckCollapsed(!deck.classList.contains("collapsed"));
});
if (window.matchMedia && window.matchMedia("(max-width: 640px)").matches) {
setDeckCollapsed(true);
}
function showError(err) {
// Do not leave an opaque loading layer above the whole application. A
// working local dataset is more useful than an error screen that prevents
// access to the controls and markers.
loadingOverlay.classList.add("hidden");
initApp(FALLBACK_DATA);
}
function loadData() {
loadingSpinner.style.display = "block";
loadingText.textContent = "Fetching live theatre data…";
loadingSub.textContent = "imax_global_database (GitHub)";
retryBtn.style.display = "none";
var controller = window.AbortController ? new AbortController() : null;
var timeout = window.setTimeout(function () {
if (controller) controller.abort();
}, 10000);
fetch(CSV_URL, { cache: "no-store", signal: controller ? controller.signal : undefined })
.then(function (res) {
if (!res.ok) throw new Error("HTTP " + res.status);
return res.text();
})
.then(function (text) {
var data = normalizeRows(parseCSV(text));
if (!data.length) throw new Error("No rows parsed from source");
window.clearTimeout(timeout);
loadingOverlay.classList.add("hidden");
initApp(data);
})
.catch(function (err) {
window.clearTimeout(timeout);
showError(err);
});
}
retryBtn.addEventListener("click", loadData);
loadData();
function initApp(DATA) {
// row: [name, city, state, country, region, lat, lng, laser, dome, f70, operator]
var INITIAL_CENTER = [20, 12];
var INITIAL_ZOOM = 2;
var map = L.map("map", {
center: INITIAL_CENTER,
zoom: INITIAL_ZOOM,
minZoom: 2,
maxZoom: 18,
zoomControl: false,
worldCopyJump: false,
maxBoundsViscosity: 1.0
});
map.setMaxBounds([[-85, -180], [85, 180]]);
// ---------- Theme-aware basemap ----------
var TILE_ATTR = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>';
var TILE_URL = {
dark: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png",
light: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"
};
function isDarkTheme() {
var t = document.documentElement.getAttribute("data-theme");
if (t === "light") return false;
if (t === "dark") return true;
return !(window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches);
}
var tileLayer = null;
function syncTileLayer() {
var url = isDarkTheme() ? TILE_URL.dark : TILE_URL.light;
if (tileLayer) map.removeLayer(tileLayer);
tileLayer = L.tileLayer(url, { subdomains: "abcd", attribution: TILE_ATTR, minZoom: 2, maxZoom: 19 });
tileLayer.addTo(map);
tileLayer.bringToBack();
}
syncTileLayer();
new MutationObserver(syncTileLayer).observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"]
});
if (window.matchMedia) {
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", syncTileLayer);
}
// ---------- Build markers ----------
var markers = new Array(DATA.length);
var markersLayer = L.layerGroup().addTo(map);
for (var i = 0; i < DATA.length; i++) {
var r = DATA[i];
var cls = "dot";
if (r[7]) cls += " laser";
if (r[8]) cls += " dome";
if (r[9]) cls += " f70";
var m = L.circleMarker([r[5], r[6]], {
radius: 4,
className: cls,
fillOpacity: 1,
opacity: 1,
bubblingMouseEvents: false
});
m._idx = i;
m.on("click", function (e) {
var rect = map.getContainer().getBoundingClientRect();
openPopoverFor(this._idx, rect.left + e.containerPoint.x, rect.top + e.containerPoint.y);
});
m.addTo(markersLayer);
markers[i] = m;
}
// ---------- Counts ----------
var regionOrder = ["APAC", "EMEA", "LATAM", "NA"];
var regionLabels = { NA: "North America" };
var regionCounts = {};
var countryByRegion = {}; // region -> Set of country
var countryCounts = {};
var fmtCounts = { laser: 0, dome: 0, f70: 0 };
DATA.forEach(function (r) {
var region = r[4] || "Other";
regionCounts[region] = (regionCounts[region] || 0) + 1;
countryCounts[r[3]] = (countryCounts[r[3]] || 0) + 1;
if (!countryByRegion[region]) countryByRegion[region] = {};
countryByRegion[region][r[3]] = true;
if (r[7]) fmtCounts.laser++;
if (r[8]) fmtCounts.dome++;
if (r[9]) fmtCounts.f70++;
});
document.getElementById("totalCount").textContent = DATA.length.toLocaleString();
document.getElementById("totalCount2").textContent = DATA.length.toLocaleString();
// ---------- Region chips ----------
var selectedRegions = new Set();
var regionChipsEl = document.getElementById("regionChips");
var regionKeys = Object.keys(regionCounts).sort(function (a, b) {
var ia = regionOrder.indexOf(a), ib = regionOrder.indexOf(b);
if (ia === -1) ia = 99; if (ib === -1) ib = 99;
return ia - ib;
});
regionKeys.forEach(function (region) {
var chip = document.createElement("div");
chip.className = "chip";
chip.dataset.region = region;
chip.innerHTML = (regionLabels[region] || region) + ' <span class="n">' + regionCounts[region].toLocaleString() + "</span>";
chip.addEventListener("click", function () {
if (selectedRegions.has(region)) selectedRegions.delete(region);
else selectedRegions.add(region);
chip.classList.toggle("on");
rebuildCountryOptions();
applyFilters(true);
});
regionChipsEl.appendChild(chip);
});
// ---------- Country select ----------
var countrySelect = document.getElementById("countrySelect");
function rebuildCountryOptions() {
var prev = countrySelect.value;
var countries;
if (selectedRegions.size === 0) {
countries = Object.keys(countryCounts);
} else {
var set = {};
selectedRegions.forEach(function (region) {
Object.keys(countryByRegion[region] || {}).forEach(function (c) { set[c] = true; });
});
countries = Object.keys(set);
}
countries.sort();
countrySelect.innerHTML = '<option value="">All countries</option>';
countries.forEach(function (c) {
var opt = document.createElement("option");
opt.value = c;
opt.textContent = c + " (" + countryCounts[c] + ")";
countrySelect.appendChild(opt);
});
if (countries.indexOf(prev) !== -1) countrySelect.value = prev;
}
rebuildCountryOptions();
countrySelect.addEventListener("change", function () { applyFilters(true); });
// ---------- Format checkboxes ----------
var formats = [
{ key: "laser", label: "Laser projection", swatch: "laser", count: fmtCounts.laser },
{ key: "dome", label: "Dome screen", swatch: "dome", count: fmtCounts.dome },
{ key: "f70", label: "70mm film", swatch: "f70", count: fmtCounts.f70 }
];
var activeFormats = {};
var formatRowEl = document.getElementById("formatRow");
formats.forEach(function (f) {
var item = document.createElement("div");
item.className = "fmt-item";
item.innerHTML =
'<span class="swatch ' + f.swatch + '"></span>' +
'<span class="fmt-text">' + f.label + "</span>" +
'<span class="fmt-count">' + f.count.toLocaleString() + "</span>";
item.addEventListener("click", function () {
activeFormats[f.key] = !activeFormats[f.key];
item.classList.toggle("on");
applyFilters(true);
});
formatRowEl.appendChild(item);
});
// ---------- Filtering ----------
var shownCountEl = document.getElementById("shownCount");
var fmtIndex = { laser: 7, dome: 8, f70: 9 };
function matchesFilters(r) {
if (selectedRegions.size > 0 && !selectedRegions.has(r[4])) return false;
if (countrySelect.value && r[3] !== countrySelect.value) return false;
for (var key in activeFormats) {
if (activeFormats[key] && !r[fmtIndex[key]]) return false;
}
return true;
}
function applyFilters(fitToResults) {
var shown = 0;
var visibleBounds = [];
for (var i = 0; i < DATA.length; i++) {
var ok = matchesFilters(DATA[i]);
if (ok) {
shown++;
visibleBounds.push([DATA[i][5], DATA[i][6]]);
}
var el = markers[i].getElement();
if (el) el.classList.toggle("dim", !ok);
}
shownCountEl.textContent = shown.toLocaleString();
if (fitToResults && visibleBounds.length) {
map.fitBounds(visibleBounds, {
paddingTopLeft: [356, 32],
paddingBottomRight: [72, 32],
maxZoom: 12,
animate: true
});
}
}
applyFilters();
document.getElementById("clearBtn").addEventListener("click", function () {
selectedRegions.clear();
document.querySelectorAll(".chip.on").forEach(function (c) { c.classList.remove("on"); });
Object.keys(activeFormats).forEach(function (k) { activeFormats[k] = false; });
document.querySelectorAll(".fmt-item.on").forEach(function (c) { c.classList.remove("on"); });
countrySelect.value = "";
searchInput.value = "";
closeSuggestions();
rebuildCountryOptions();
applyFilters(true);
});
// ---------- Search ----------
var searchInput = document.getElementById("searchInput");
var suggestionsEl = document.getElementById("suggestions");
function closeSuggestions() {
suggestionsEl.classList.remove("open");
suggestionsEl.innerHTML = "";
}
searchInput.addEventListener("input", function () {
var q = searchInput.value.trim().toLowerCase();
if (!q) { closeSuggestions(); return; }
var matches = [];
for (var i = 0; i < DATA.length && matches.length < 8; i++) {
var r = DATA[i];
var hay = (r[0] + " " + r[1] + " " + r[3]).toLowerCase();
if (hay.indexOf(q) !== -1) matches.push(i);
}
if (!matches.length) { closeSuggestions(); return; }
suggestionsEl.innerHTML = "";
matches.forEach(function (idx) {
var r = DATA[idx];
var item = document.createElement("div");
item.className = "sugg-item";
item.innerHTML =
'<div class="sugg-name">' + escapeHtml(r[0]) + "</div>" +
'<div class="sugg-meta">' + escapeHtml([r[1], r[3]].filter(Boolean).join(", ")) + "</div>";
item.addEventListener("click", function () {
closeSuggestions();
searchInput.value = r[0];
flyTo(idx);
});
suggestionsEl.appendChild(item);
});
suggestionsEl.classList.add("open");
});
document.addEventListener("click", function (e) {
if (!e.target.closest(".search-wrap")) closeSuggestions();
});
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
});
}
// ---------- Zoom controls ----------
document.getElementById("zoomIn").addEventListener("click", function () { map.zoomIn(); });
document.getElementById("zoomOut").addEventListener("click", function () { map.zoomOut(); });
document.getElementById("zoomReset").addEventListener("click", function () {
closePopover();
map.setView(INITIAL_CENTER, INITIAL_ZOOM);
});
// ---------- Popover ----------
var popover = document.getElementById("popover");
var popName = document.getElementById("popName");
var popLoc = document.getElementById("popLoc");
var popBadges = document.getElementById("popBadges");
var activeMarkerEl = null;
function closePopover() {
popover.classList.remove("open");
if (activeMarkerEl) { activeMarkerEl.classList.remove("active"); activeMarkerEl = null; }
}
document.getElementById("popClose").addEventListener("click", closePopover);
function openPopoverFor(idx, clientX, clientY) {
var r = DATA[idx];
if (activeMarkerEl) activeMarkerEl.classList.remove("active");
activeMarkerEl = markers[idx].getElement();
if (activeMarkerEl) activeMarkerEl.classList.add("active");
popName.textContent = r[0];
var locBits = [r[1], r[2], r[3]].filter(Boolean);
popLoc.textContent = locBits.join(", ") + (r[10] ? " · " + r[10] : "");
var badges = [];
if (r[7]) badges.push('<span class="badge laser">Laser</span>');
if (r[8]) badges.push('<span class="badge dome">Dome</span>');
if (r[9]) badges.push('<span class="badge f70">70mm</span>');
if (!badges.length) badges.push('<span class="badge">Digital IMAX</span>');
popBadges.innerHTML = badges.join("");
popover.classList.add("open");
var vw = window.innerWidth, vh = window.innerHeight;
var pw = popover.offsetWidth, ph = popover.offsetHeight;
var left = Math.min(Math.max(8, clientX + 14), vw - pw - 8);
var top = Math.min(Math.max(8, clientY - ph / 2), vh - ph - 8);
popover.style.left = left + "px";
popover.style.top = top + "px";
}
map.on("click", closePopover);
map.on("movestart zoomstart", closePopover);
function flyTo(idx) {
var r = DATA[idx];
var latlng = [r[5], r[6]];
map.flyTo(latlng, Math.max(map.getZoom(), 8), { duration: 0.8 });
map.once("moveend", function () {
var pt = map.latLngToContainerPoint(latlng);
var rect = map.getContainer().getBoundingClientRect();
openPopoverFor(idx, rect.left + pt.x, rect.top + pt.y);
});
}
} // end initApp
})();
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment