sw-fe/src/components/Map/markerMedia.js

264 lines
8.5 KiB
JavaScript

// markerMedia.js - Media, image, video, gallery handling
import { escapeHtml } from "./markerUtils";
export function normalizeImageUrl(raw) {
const value = (raw || "").toString().trim();
if (!value) return "";
if (value.startsWith("//")) return `https:${value}`;
if (value.startsWith("http://")) return `https://${value.slice(7)}`;
return value;
}
export function normalizeMediaUrl(raw) {
const value = (raw || "").toString().trim();
if (!value) return "";
if (value.startsWith("//")) return `https:${value}`;
if (value.startsWith("http://")) return `https://${value.slice(7)}`;
return value;
}
export function resolvePostImage(post) {
return normalizeImageUrl(
post?.image_small ||
post?.image_medium ||
post?.image_large ||
post?.image ||
post?.media_url ||
""
);
}
export function heroImageForPost(post) {
const raw =
post?.image_medium ||
post?.image_large ||
post?.image_small ||
post?.image ||
post?.media_url ||
"";
const v = normalizeImageUrl(raw);
if (v) return v;
const cat = (post?.category || "").toString().trim();
return `/og/category?cat=${encodeURIComponent(cat || "NEWS")}`;
}
export function fullImageForPost(post) {
const raw =
post?.image_large ||
post?.image_medium ||
post?.image_small ||
post?.image ||
post?.media_url ||
"";
return normalizeImageUrl(raw);
}
export function collectGalleryImages(post) {
const result = [];
const seen = new Set();
const add = (value) => {
const normalized = normalizeImageUrl(value);
if (!normalized) return;
if (seen.has(normalized)) return;
seen.add(normalized);
result.push(normalized);
};
if (Array.isArray(post?.media_urls)) {
post.media_urls.forEach(add);
}
["image_large", "image_medium", "image_small", "image", "media_url"].forEach((key) => {
add(post?.[key]);
});
return result;
}
// Extract camera ID from Quebec 511 URL
export function extractCameraId(url) {
if (!url) return null;
const match = url.match(/[?&]id=(\d+)/);
return match ? match[1] : null;
}
// Build stream proxy URL for camera
export function getCameraStreamUrl(postData) {
const embedUrl = normalizeMediaUrl(postData?.embed_url || postData?.video_url || '');
const cameraId = extractCameraId(embedUrl);
if (cameraId) {
return `/live/api/stream/${cameraId}`;
}
return embedUrl;
}
export function renderTagPills(tags, topicImpacts = {}) {
if (!Array.isArray(tags) || tags.length === 0) return "";
const uniq = [];
const seen = new Set();
for (const raw of tags) {
const t = (raw || "").toString().trim().toLowerCase();
if (!t || seen.has(t)) continue;
seen.add(t);
uniq.push(t);
if (uniq.length >= 6) break;
}
if (uniq.length === 0) return "";
// Get impact color class: negative=red, neutral=blue, positive=green
const getImpactClass = (tag) => {
const impact = topicImpacts?.[tag] ?? topicImpacts?.[tag.toLowerCase()] ?? 0;
if (impact < 0) return "sw-tag-negative";
if (impact > 0) return "sw-tag-positive";
return "sw-tag-neutral";
};
return `
<div class="sw-modal-tags">
${uniq.map((t) => `<span class="sw-modal-tag ${getImpactClass(t)}">#${escapeHtml(t)}</span>`).join("")}
</div>
`;
}
export function setModalTags(modalWrap, post) {
if (!modalWrap) return;
const tagsHtml = renderTagPills(post?.event_tags || post?.tags, post?.topic_impacts);
const existing = modalWrap.querySelector(".sw-modal-tags");
if (tagsHtml) {
if (existing) {
existing.outerHTML = tagsHtml;
} else {
const titleEl = modalWrap.querySelector(".sw-modal-title") || modalWrap.querySelector(".sw-cluster-title");
if (titleEl) titleEl.insertAdjacentHTML("afterend", tagsHtml);
}
} else if (existing) {
existing.remove();
}
}
export function bindHeroLightbox(modalWrap) {
if (!modalWrap) return;
const img = modalWrap.querySelector(".sw-modal-hero img, .sw-cluster-hero-media img");
if (!img || img.dataset.zoomBound === "1") return;
img.dataset.zoomBound = "1";
img.style.cursor = "zoom-in";
img.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
const src = (img.getAttribute("data-fullsrc") || img.getAttribute("src") || "").trim();
if (src) {
openImageLightbox(src);
}
});
}
export function initModalGallery(modalWrap, gallery) {
if (!modalWrap || !Array.isArray(gallery) || gallery.length === 0) return;
const heroImg = modalWrap.querySelector(".sw-modal-hero img, .sw-cluster-hero-media img");
if (!heroImg) return;
const arrows = modalWrap.querySelectorAll(".sw-modal-hero-arrow");
let activeIndex = 0;
const indicator = modalWrap.querySelector(".sw-modal-gallery-indicator");
const currentIndicator = indicator?.querySelector(".sw-modal-gallery-current");
const totalIndicator = indicator?.querySelector(".sw-modal-gallery-total");
if (totalIndicator) {
totalIndicator.textContent = String(gallery.length);
}
const setActive = (idx) => {
if (!Number.isFinite(idx)) return;
const normalized = (idx + gallery.length) % gallery.length;
activeIndex = normalized;
heroImg.src = gallery[normalized];
heroImg.dataset.fullsrc = gallery[normalized];
heroImg.dataset.galleryIndex = String(normalized);
if (currentIndicator) {
currentIndicator.textContent = String(normalized + 1);
}
};
arrows.forEach((arrow) => {
const dir = arrow.classList.contains("sw-modal-hero-arrow-prev") ? -1 : 1;
arrow.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
setActive(activeIndex + dir);
});
});
// Touch swipe for gallery
let touchStartX = 0;
const touchThreshold = 50;
heroImg.addEventListener("touchstart", (event) => {
touchStartX = event.touches[0].clientX;
});
heroImg.addEventListener("touchend", (event) => {
const dx = event.changedTouches[0].clientX - touchStartX;
if (Math.abs(dx) < touchThreshold) return;
const direction = dx > 0 ? -1 : 1;
const nextIndex = (activeIndex + direction + gallery.length) % gallery.length;
setActive(nextIndex);
});
}
export function openImageLightbox(src) {
if (!src) return;
const overlay = document.createElement("div");
overlay.className = "sw-image-lightbox";
overlay.style.position = "fixed";
overlay.style.inset = "0";
overlay.style.background = "rgba(8,10,16,0.86)";
overlay.style.zIndex = "6000"; /* Image lightbox - above post modal (5000), below auth (10000) */
overlay.style.display = "flex";
overlay.style.alignItems = "center";
overlay.style.justifyContent = "center";
overlay.style.padding = "16px";
const img = document.createElement("img");
img.src = src;
img.alt = "";
img.style.maxWidth = "96vw";
img.style.maxHeight = "96vh";
img.style.objectFit = "contain";
img.style.borderRadius = "14px";
img.style.boxShadow = "0 20px 50px rgba(0,0,0,0.45)";
overlay.appendChild(img);
const close = (opts = {}) => {
try { document.removeEventListener("keydown", onKey); } catch {}
try { window.removeEventListener("popstate", onPop); } catch {}
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
if (!opts.skipHistory && overlay.__swPushed) {
overlay.__swPushed = false;
try { history.back(); } catch {}
}
};
const onKey = (e) => {
if (e.key === "Escape") close();
};
const onPop = () => close({ skipHistory: true });
overlay.addEventListener("click", close);
document.addEventListener("keydown", onKey);
window.addEventListener("popstate", onPop);
try {
history.pushState({ sw: "image" }, "");
overlay.__swPushed = true;
} catch {}
document.body.appendChild(overlay);
}
export function buildClusterItemsHtml(items) {
if (!Array.isArray(items) || items.length === 0) return "";
const rows = items.slice(0, 10).map((item) => {
const title = escapeHtml(item?.title || "");
const source = escapeHtml(item?.source || "");
const url = item?.url || "";
const img = normalizeImageUrl(item?.image_small || item?.image_medium || item?.image_large || "");
return `
<a class="sw-cluster-row" href="${escapeHtml(url)}" target="_blank" rel="noopener" title="${title}">
${img ? `<img src="${escapeHtml(img)}" class="sw-cluster-row-thumb" alt="" loading="lazy" />` : `<div class="sw-cluster-row-thumb sw-cluster-row-thumb--empty"></div>`}
<div class="sw-cluster-row-info">
<div class="sw-cluster-row-title">${title}</div>
<div class="sw-cluster-row-source">${source}</div>
</div>
</a>
`;
});
return rows.join("");
}