const { form, button, div, h2, h3, p, section, input, label, br, a, span, textarea, select, option, img, strong } = require("../server/node_modules/hyperaxe"); const moment = require("../server/node_modules/moment"); const { template, i18n, userLink} = require("./main_views"); const { config } = require("../server/SSB_server.js"); const { renderMapWithPins, renderZoomedMapWithPins, getViewportBounds, latLngToPx, pxToLatLng, MAP_W, MAP_H, getMaxTileZoom } = require("../maps/map_renderer"); const { sanitizeHtml } = require('../backend/sanitizeHtml'); const userId = config.keys.id; const safeArr = (v) => (Array.isArray(v) ? v : []); const safeText = (v) => String(v || "").trim(); const buildReturnTo = (filter, params = {}) => { const f = safeText(filter || "all"); const q = safeText(params.q || ""); const parts = [`filter=${encodeURIComponent(f)}`]; if (q) parts.push(`q=${encodeURIComponent(q)}`); return `/maps?${parts.join("&")}`; }; const renderPMButton = (recipient) => { const r = safeText(recipient); if (!r || String(r) === String(userId)) return null; return form({ method: "GET", action: "/pm" }, input({ type: "hidden", name: "recipients", value: r }), button({ type: "submit", class: "filter-btn" }, i18n.privateMessage)); }; const renderTags = (tags) => { const list = safeArr(tags).map((t) => String(t || "").trim()).filter(Boolean); return list.length ? div({ class: "card-tags" }, list.map((tag) => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))) : null; }; const renderMapFavoriteToggle = (mapObj, returnTo = "") => form({ method: "POST", action: mapObj.isFavorite ? `/maps/favorites/remove/${encodeURIComponent(mapObj.key)}` : `/maps/favorites/add/${encodeURIComponent(mapObj.key)}` }, returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, button({ type: "submit", class: "filter-btn" }, mapObj.isFavorite ? i18n.mapRemoveFavoriteButton : i18n.mapAddFavoriteButton)); let areaCounter = 0; const buildAreas = (clickUrl, latParam = "lat", lngParam = "lng", viewport = null) => { const GRID = 16; const cellW = MAP_W / GRID; const cellH = MAP_H / GRID; const areas = []; for (let gy = 0; gy < GRID; gy++) { for (let gx = 0; gx < GRID; gx++) { let c; if (viewport) { const lat = viewport.latMax - (gy + 0.5) / GRID * (viewport.latMax - viewport.latMin); const lng = viewport.lngMin + (gx + 0.5) / GRID * (viewport.lngMax - viewport.lngMin); c = { lat: Math.round(lat * 10000) / 10000, lng: Math.round(lng * 10000) / 10000 }; } else { const cx = Math.round(gx * cellW + cellW / 2); const cy = Math.round(gy * cellH + cellH / 2); c = pxToLatLng(cx, cy); } const x1 = Math.round(gx * cellW); const y1 = Math.round(gy * cellH); const x2 = Math.round((gx + 1) * cellW); const y2 = Math.round((gy + 1) * cellH); areas.push(`${c.lat},${c.lng}`); } } return areas; }; const renderMap = (markers, clickUrl, mainIdx, opts = {}) => { areaCounter++; const mapName = `m${areaCounter}`; const latParam = opts.latParam || "lat"; const lngParam = opts.lngParam || "lng"; const pinLabels = opts.pinLabels || []; const pinImages = opts.pinImages || []; const pfx = opts.pinPrefix || `pin${areaCounter}`; const zoom = parseInt(opts.zoom) || 2; const centerLat = typeof opts.centerLat === "number" ? opts.centerLat : 0; const centerLng = typeof opts.centerLng === "number" ? opts.centerLng : 0; const pinList = safeArr(markers).filter((m) => m && typeof m.lat === "number" && typeof m.lng === "number"); const useZoom = zoom > 2; const mapFile = useZoom ? renderZoomedMapWithPins(centerLat, centerLng, zoom, pinList, mainIdx) : (pinList.length > 0 ? renderMapWithPins(pinList, mainIdx) : null); const imgSrc = mapFile ? `/mapcache/${mapFile}` : "/assets/images/worldmap-z2.png"; const viewport = useZoom && clickUrl ? getViewportBounds(centerLat, centerLng, zoom) : null; const useMap = clickUrl || pinLabels.length > 0; const mapTag = useMap ? mapName : ""; let gridAreasHtml = ""; if (clickUrl) { const clickUrlWithZoom = zoom > 2 ? `${clickUrl}zoom=${zoom}&` : clickUrl; gridAreasHtml = buildAreas(clickUrlWithZoom, latParam, lngParam, viewport).join(""); } let popupAreasHtml = ""; let popupsHtml = ""; if (pinLabels.length > 0) { const vp = useZoom ? getViewportBounds(centerLat, centerLng, zoom) : null; pinList.forEach((m, i) => { const lbl = pinLabels[i] || ""; let px; if (vp) { px = { x: ((m.lng - vp.lngMin) / (vp.lngMax - vp.lngMin)) * MAP_W, y: ((vp.latMax - m.lat) / (vp.latMax - vp.latMin)) * MAP_H }; } else { px = latLngToPx(m.lat, m.lng); } const escaped = lbl.replace(/&/g, "&").replace(/"/g, """).replace(//g, ">"); const withLinks = escaped.replace(/https?:\/\/[^\s&"<>]+/g, (url) => { const clean = url.replace(/&/g, "&"); return `${url}`; }).replace(/\n/g, "
"); const sz = 20 * Math.pow(2, Math.max(0, zoom - getMaxTileZoom())); const x1 = Math.max(0, px.x - sz); const y1 = Math.max(0, px.y - sz); const x2 = Math.min(MAP_W, px.x + sz); const y2 = Math.min(MAP_H, px.y + sz); const popupId = `${pfx}_${i}`; const latStr = typeof m.lat === "number" ? m.lat.toFixed(4) : ""; const lngStr = typeof m.lng === "number" ? m.lng.toFixed(4) : ""; const imgBlobId = pinImages[i] && String(pinImages[i]).startsWith("&") ? pinImages[i] : ""; const imgHtml = imgBlobId ? `` : ""; popupAreasHtml += `${escaped}`; popupsHtml += `
${imgHtml}
${sanitizeHtml(withLinks)}
${latStr}, ${lngStr}
`; }); } const mapHtml = useMap ? `${popupAreasHtml}${gridAreasHtml}` : ""; const useAttr = useMap ? ` usemap="#${mapTag}"` : ""; const mapWrapHtml = `
map${mapHtml}
`; const viewerEl = div({ class: "map-viewer" }, { innerHTML: mapWrapHtml }); if (!popupsHtml) return viewerEl; return div({ class: "map-zone" }, viewerEl, div({ class: "map-popup-container", innerHTML: popupsHtml })); }; const renderCoordPreview = (lat, lng) => { if (!lat && !lng) return null; return span({ class: "map-coord-inline" }, span({ class: "map-coord-pin" }, "๐Ÿ“"), strong(`${lat}, ${lng}`)); }; const renderLocalEmbed = (lat, lng) => { const la = parseFloat(lat) || 0; const lo = parseFloat(lng) || 0; if (!la && !lo) return null; return renderMap([{ lat: la, lng: lo }], null, 0); }; const renderMapUrl = (mapObj) => div({ class: "map-url-container" }, span({ class: "card-label" }, i18n.mapUrlLabel + ": "), a({ href: `/maps/${encodeURIComponent(mapObj.key)}`, class: "map-url-link" }, `/maps/${encodeURIComponent(mapObj.key)}`)); const renderMapOwnerActions = (filter, mapObj, params = {}) => { const returnTo = buildReturnTo(filter, params); if (String(mapObj.author) !== String(userId)) return []; return [ form({ method: "GET", action: `/maps/edit/${encodeURIComponent(mapObj.key)}` }, input({ type: "hidden", name: "returnTo", value: returnTo }), button({ class: "update-btn", type: "submit" }, i18n.mapUpdateButton)), form({ method: "POST", action: `/maps/delete/${encodeURIComponent(mapObj.key)}` }, input({ type: "hidden", name: "returnTo", value: returnTo }), button({ class: "delete-btn", type: "submit" }, i18n.mapDeleteButton)) ]; }; const renderFilters = (filter, q) => div({ class: "filters" }, form({ method: "GET", action: "/maps", class: "ui-toolbar ui-toolbar--filters" }, input({ type: "hidden", name: "q", value: q || "" }), button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, i18n.mapFilterAll), button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.mapFilterMine), button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.mapFilterRecent), button({ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, i18n.mapFilterFavorites), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.mapUploadButton))); const renderMapForm = (filter, mapId, mapToEdit, params = {}) => { const returnFilter = filter === "create" ? "all" : params.filter || "all"; const returnTo = safeText(params.returnTo) || buildReturnTo(returnFilter, params); const latVal = params.lat !== undefined ? String(params.lat) : String(mapToEdit?.lat || ""); const lngVal = params.lng !== undefined ? String(params.lng) : String(mapToEdit?.lng || ""); const titleVal = params.title || mapToEdit?.title || ""; const descVal = params.description || mapToEdit?.description || ""; const markerLabelVal = params.markerLabel !== undefined ? params.markerLabel : (mapToEdit?.markerLabel || ""); const tagsValue = params.tags !== undefined ? params.tags : safeArr(mapToEdit?.tags).join(", "); const mapTypeVal = params.mapType || mapToEdit?.mapType || "SINGLE"; const maxTileZoom = getMaxTileZoom(); const zoomVal = parseInt(params.zoom) || 2; const cleanUrl = `/maps?filter=create${params.tribeId ? '&tribeId=' + encodeURIComponent(params.tribeId) : ''}`; const pickerMarkers = latVal && lngVal ? [{ lat: parseFloat(latVal), lng: parseFloat(lngVal) }] : []; return div({ class: "map-create-layout" }, div({ class: "map-form map-form-full" }, form({ action: filter === "edit" ? `/maps/update/${encodeURIComponent(mapId)}` : "/maps/create", method: "POST", enctype: "multipart/form-data" }, input({ type: "hidden", name: "returnTo", value: returnTo }), input({ type: "hidden", name: "filter", value: "create" }), params.tribeId ? input({ type: "hidden", name: "tribeId", value: params.tribeId }) : null, label(i18n.title || "Title"), input({ type: "text", name: "title", placeholder: i18n.mapTitlePlaceholder || "Map title", value: titleVal }), label(i18n.mapDescriptionLabel), textarea({ name: "description", placeholder: i18n.mapDescriptionPlaceholder, rows: "3" }, descVal), label(i18n.mapTagsLabel), input({ type: "text", name: "tags", placeholder: i18n.mapTagsPlaceholder, value: tagsValue }), label(i18n.mapTypeLabel), select({ name: "mapType" }, option({ value: "SINGLE", ...(mapTypeVal === "SINGLE" ? { selected: true } : {}) }, "SINGLE"), option({ value: "OPEN", ...(mapTypeVal === "OPEN" ? { selected: true } : {}) }, "OPEN"), option({ value: "CLOSED", ...(mapTypeVal === "CLOSED" ? { selected: true } : {}) }, "CLOSED")), br(),br(), label(i18n.mapMarkerLabelField), textarea({ name: "markerLabel", placeholder: i18n.mapMarkerLabelPlaceholder, rows: "3" }, markerLabelVal), label(i18n.markerImageLabel || "Marker Image"), input({ type: "file", name: "image", accept: "image/*" }), br(), br(), label(i18n.mapLatLabel), input({ type: "text", name: "lat", placeholder: i18n.mapLatPlaceholder, value: latVal }), label(i18n.mapLngLabel), input({ type: "text", name: "lng", placeholder: i18n.mapLngPlaceholder, value: lngVal }), div({ class: "map-form-row" }, button({ type: "submit", attrs: { formmethod: "GET" }, formaction: "/maps", class: "filter-btn" }, i18n.mapAddMarkerButton || "Add Marker"), a({ href: cleanUrl, class: "filter-btn" }, i18n.mapCleanMarkerButton || "Clean Marker")), renderCoordPreview(latVal, lngVal), label(i18n.mapZoomLabel || "Zoom"), select({ name: "zoom" }, [2, 3, 4, 5, 6, 7, 8].map(z => option({ value: String(z), ...(zoomVal === z ? { selected: true } : {}) }, String(z)))), br(),br(), button({ type: "submit", attrs: { formmethod: "GET" }, formaction: "/maps", class: "filter-btn" }, i18n.mapApplyZoom || "Apply Zoom"), div({ class: "map-form-map-slot" }, renderMap(pickerMarkers, null, 0, { zoom: zoomVal, centerLat: parseFloat(latVal) || 0, centerLng: parseFloat(lngVal) || 0 })), button({ type: "submit", class: "create-button" }, filter === "edit" ? i18n.mapUpdateButton : i18n.mapCreateButton)))); }; const renderMarkerForm = (mapObj, returnTo, params = {}, tribeMembers = []) => { if (mapObj.mapType === "SINGLE") return null; if (mapObj.mapType === "CLOSED" && String(mapObj.author) !== String(userId)) return null; if (mapObj.mapType === "OPEN" && mapObj.tribeId && !tribeMembers.includes(userId)) return null; const mkLat = params.mkLat || ""; const mkLng = params.mkLng || ""; const zoomVal = parseInt(params.zoom) || 2; const existingMarkers = [{ lat: mapObj.lat, lng: mapObj.lng }].concat( safeArr(mapObj.markers).map((m) => ({ lat: m.lat, lng: m.lng }))); if (mkLat && mkLng) existingMarkers.push({ lat: parseFloat(mkLat), lng: parseFloat(mkLng) }); const pinLabels = [mapObj.markerLabel || mapObj.description || mapObj.title || ""].concat( safeArr(mapObj.markers).map((m) => m.label || "")); const mkCleanUrl = `/maps/${encodeURIComponent(mapObj.key)}?filter=${encodeURIComponent(params.filter || "all")}`; const clickUrl = `/maps/${encodeURIComponent(mapObj.key)}?filter=${encodeURIComponent(params.filter || "all")}&zoom=${zoomVal}&`; return div({ class: "map-marker-form", id: "add-marker" }, h3(i18n.mapAddMarkerTitle), form({ method: "POST", action: `/maps/${encodeURIComponent(mapObj.key)}/marker`, class: "map-form", enctype: "multipart/form-data" }, returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, label(i18n.mapMarkerLabelField), textarea({ name: "label", placeholder: i18n.mapMarkerLabelPlaceholder, rows: "3" }, params.mkMarkerLabel || ""), label(i18n.markerImageLabel || "Marker Image"), input({ type: "file", name: "image", accept: "image/*" }), br(),br(), label(i18n.mapMarkerLatLabel), input({ type: "text", name: "mkLat", placeholder: i18n.mapLatPlaceholder, value: String(mkLat) }), label(i18n.mapMarkerLngLabel), input({ type: "text", name: "mkLng", placeholder: i18n.mapLngPlaceholder, value: String(mkLng) }), div({ class: "map-form-row" }, button({ type: "submit", attrs: { formmethod: "GET" }, formaction: `/maps/${encodeURIComponent(mapObj.key)}`, class: "filter-btn" }, i18n.mapAddMarkerButton || "Add Marker"), a({ href: mkCleanUrl, class: "filter-btn" }, i18n.mapCleanMarkerButton || "Clean Marker")), renderCoordPreview(mkLat, mkLng), label(i18n.mapZoomLabel || "Zoom"), select({ name: "zoom" }, [2, 3, 4, 5, 6, 7, 8].map(z => option({ value: String(z), ...(zoomVal === z ? { selected: true } : {}) }, String(z)))), br(),br(), button({ type: "submit", attrs: { formmethod: "GET" }, formaction: `/maps/${encodeURIComponent(mapObj.key)}`, class: "filter-btn" }, i18n.mapApplyZoom || "Apply Zoom"), div({ class: "map-form-map-slot" }, renderMap(existingMarkers, clickUrl, 0, { latParam: "mkLat", lngParam: "mkLng", pinLabels, pinPrefix: `mk${areaCounter}`, zoom: zoomVal, centerLat: parseFloat(mkLat) || parseFloat(mapObj.lat) || 0, centerLng: parseFloat(mkLng) || parseFloat(mapObj.lng) || 0 })), button({ type: "submit", class: "create-button" }, i18n.mapAddMarkerButton))); }; const renderMarkersList = (markers, mapObj) => { const allMarkers = []; if (mapObj) { allMarkers.push({ lat: mapObj.lat, lng: mapObj.lng, label: mapObj.markerLabel || mapObj.description || mapObj.title || i18n.mapMarkerDefault, author: mapObj.author, createdAt: mapObj.createdAt }); } allMarkers.push(...safeArr(markers)); if (!allMarkers.length) return null; return div({ class: "map-markers-list" }, h3(i18n.mapMarkersTitle), br(), div(allMarkers.flatMap((mk, i) => [ ...(i > 0 ? [br()] : []), div({ class: "map-marker-info" }, span({ class: "map-marker-dot" }, "๊”Œ"), span({ class: "map-marker-coords" }, `${(typeof mk.lat === 'number' ? mk.lat : 0).toFixed(4)}, ${(typeof mk.lng === 'number' ? mk.lng : 0).toFixed(4)}`), span({ class: "map-marker-meta" }, userLink(mk.author), ` ยท ${moment(mk.createdAt).fromNow()}`)) ]))); }; const renderMapCard = (mapObj, filter, params = {}) => { const returnTo = buildReturnTo(filter, params); const ownerActions = renderMapOwnerActions(filter, mapObj, params); const markerCount = safeArr(mapObj.markers).length; const thumbMarkers = [{ lat: mapObj.lat, lng: mapObj.lng }].concat( safeArr(mapObj.markers).map((m) => ({ lat: m.lat, lng: m.lng }))); const thumbFile = renderMapWithPins(thumbMarkers, 0); const thumbSrc = thumbFile ? `/mapcache/${thumbFile}` : "/assets/images/worldmap-z2.png"; return div({ class: "map-card" }, a({ href: `/maps/${encodeURIComponent(mapObj.key)}?filter=${encodeURIComponent(filter)}`, class: "map-card-thumb-link" }, { innerHTML: `map` }), div({ class: "map-card-body" }, mapObj.title ? h2(a({ href: `/maps/${encodeURIComponent(mapObj.key)}?filter=${encodeURIComponent(filter)}` }, mapObj.title)) : null, div({ class: "map-card-header" }, div({ class: "map-card-info" }, span({ class: "map-type-badge" }, mapObj.mapType), span({ class: "map-coords" }, `๐Ÿ“ ${mapObj.lat.toFixed(4)}, ${mapObj.lng.toFixed(4)}`), markerCount > 0 ? span({ class: "map-marker-count" }, `โ–พ ${markerCount}`) : null, mapObj.key ? renderMapUrl(mapObj) : null), div({ class: "map-card-actions" }, form({ method: "GET", action: `/maps/${encodeURIComponent(mapObj.key)}` }, input({ type: "hidden", name: "returnTo", value: returnTo }), input({ type: "hidden", name: "filter", value: filter || "all" }), button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)), renderMapFavoriteToggle(mapObj, returnTo), renderPMButton(mapObj.author), ...ownerActions)), safeText(mapObj.description) ? p({ class: "map-description" }, mapObj.description) : null, p({ class: "card-footer" }, span({ class: "date-link" }, moment(mapObj.createdAt).fromNow()), span(" ยท "), userLink(mapObj.author)))); }; const renderMapList = (maps, filter, params = {}) => maps.length ? maps.map((mapObj) => renderMapCard(mapObj, filter, params)) : p(params.q ? i18n.mapNoMatch : i18n.noMaps); exports.mapsView = async (maps, filter = "all", mapId = null, params = {}) => { const title = filter === "mine" ? i18n.mapMineSectionTitle : filter === "create" ? i18n.mapCreateSectionTitle : filter === "edit" ? i18n.mapUpdateSectionTitle : filter === "recent" ? i18n.mapRecentSectionTitle : filter === "favorites" ? i18n.mapFavoritesSectionTitle : i18n.mapAllSectionTitle; const q = safeText(params.q || ""); const list = safeArr(maps); const mapToEdit = mapId ? list.find((m) => m.key === mapId) : null; const allMarkers = list.map((m) => ({ lat: m.lat, lng: m.lng, href: `/maps/${encodeURIComponent(m.key)}` })); return template(title, section( div({ class: "tags-header" }, h2(title), p(i18n.mapDescription)), renderFilters(filter, q)), section( filter === "create" || filter === "edit" ? renderMapForm(filter, mapId, mapToEdit, { ...params, filter }) : section( div({ class: "maps-search" }, form({ method: "GET", action: "/maps", class: "filter-box" }, input({ type: "hidden", name: "filter", value: filter }), input({ type: "text", name: "q", value: q, placeholder: i18n.mapSearchPlaceholder, class: "filter-box__input" }), div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.mapSearchButton)))), div({ class: "maps-list" }, renderMapList(list, filter, { q }))))); }; exports.singleMapView = async (mapObj, filter = "all", params = {}) => { const q = safeText(params.q || ""); const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q }); const ownerActions = renderMapOwnerActions(filter, mapObj, { q }); const tribeMembers = safeArr(params.tribeMembers); const zoomVal = parseInt(params.zoom) || 2; const allMarkers = [{ lat: mapObj.lat, lng: mapObj.lng }].concat( safeArr(mapObj.markers).map((m) => ({ lat: m.lat, lng: m.lng }))); const pinLabels = [mapObj.markerLabel || mapObj.description || mapObj.title || ""].concat( safeArr(mapObj.markers).map((m) => m.label || "")); const pinImages = [mapObj.image || ""].concat(safeArr(mapObj.markers).map((m) => m.image || "")); return template(mapObj.title || i18n.mapTitle, section(renderFilters(filter, q)), section( div({ class: "map-detail" }, mapObj.title ? h2(mapObj.title) : null, safeText(mapObj.description) ? p({ class: "map-description" }, mapObj.description) : null, div({ class: "map-detail-header" }, div({ class: "map-detail-info" }, span({ class: "map-type-badge" }, mapObj.mapType), span({ class: "map-coords-detail" }, `๐Ÿ“ ${mapObj.lat.toFixed(6)}, ${mapObj.lng.toFixed(6)}`)), div({ class: "map-detail-actions" }, renderMapFavoriteToggle(mapObj, returnTo), renderPMButton(mapObj.author), ...ownerActions)), renderMapUrl(mapObj), br(), form({ method: "GET", action: `/maps/${encodeURIComponent(mapObj.key)}` }, label(i18n.mapZoomLabel || "Zoom"), br(), select({ name: "zoom" }, [2, 3, 4, 5, 6, 7, 8].map(z => option({ value: String(z), ...(zoomVal === z ? { selected: true } : {}) }, String(z)))), br(), br(), button({ type: "submit", class: "filter-btn" }, i18n.mapApplyZoom || "Apply Zoom")), br(), renderMap(allMarkers, null, 0, { pinLabels, pinImages, pinPrefix: `detail${areaCounter}`, zoom: zoomVal, centerLat: parseFloat(mapObj.lat) || 0, centerLng: parseFloat(mapObj.lng) || 0 }), renderMarkersList(mapObj.markers, mapObj), renderTags(mapObj.tags), br(), p({ class: "card-footer" }, span({ class: "date-link" }, `${moment(mapObj.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), userLink(mapObj.author), mapObj.updatedAt && mapObj.updatedAt !== mapObj.createdAt ? span({ class: "votations-comment-date" }, ` ยท ${i18n.mapUpdatedAt}: ${moment(mapObj.updatedAt).format("YYYY/MM/DD HH:mm:ss")}`) : null), renderMarkerForm(mapObj, returnTo, params, tribeMembers)))); }; exports.renderMapLocationUrl = (mapUrl) => { if (!mapUrl) return null; return span({ class: "map-location-inline" }, span({ class: "map-location-icon" }, "๊”Œ"), a({ href: mapUrl, class: "map-location-link" }, mapUrl)); }; exports.renderMapLocationVisitLabel = (mapUrl) => { if (!mapUrl) return null; return div({ class: "card-field" }, span({ class: "card-label" }, (i18n.mapLocationTitle || "Map Location") + ":"), span({ class: "card-value" }, a({ href: mapUrl, class: "map-location-link" }, i18n.mapVisitLabel || "Visit map"))); }; exports.renderMapEmbed = (mapData, mapUrl) => { if (!mapData || (parseFloat(mapData.lat) === 0 && parseFloat(mapData.lng) === 0)) return exports.renderMapLocationVisitLabel(mapUrl); return div({ class: "map-embed-section" }, span({ class: "card-label" }, (i18n.mapLocationTitle || "Map Location") + ":"), span({ class: "card-value map-zoom-info" }, "Zoom: 2"), renderLocalEmbed(mapData.lat, mapData.lng), mapUrl ? div({ class: "map-embed-url" }, a({ href: mapUrl, class: "map-location-link" }, mapUrl)) : null); }; exports.renderMapEmbedWithZoom = (mapData, mapUrl, detailUrl, zoom) => { if (!mapData || (parseFloat(mapData.lat) === 0 && parseFloat(mapData.lng) === 0)) return exports.renderMapLocationVisitLabel(mapUrl); const zoomVal = parseInt(zoom) || 2; const la = parseFloat(mapData.lat) || 0; const lo = parseFloat(mapData.lng) || 0; return div({ class: "map-embed-section" }, span({ class: "card-label" }, (i18n.mapLocationTitle || "Map Location") + ":"), form({ method: "GET", action: detailUrl }, label(i18n.mapZoomLabel || "Zoom"), br(), select({ name: "zoom" }, [2, 3, 4, 5, 6, 7, 8].map(z => option({ value: String(z), ...(zoomVal === z ? { selected: true } : {}) }, String(z)))), br(), br(), button({ type: "submit", class: "filter-btn" }, i18n.mapApplyZoom || "Apply Zoom")), br(), renderMap([{ lat: la, lng: lo }], null, 0, { zoom: zoomVal, centerLat: la, centerLng: lo }), mapUrl ? div({ class: "map-embed-url" }, a({ href: mapUrl, class: "map-location-link" }, mapUrl)) : null); }; exports.renderMapLocationGrid = (lat, lng) => { if (lat === undefined || lng === undefined) return null; return div({ class: "map-location-embed" }, renderMap([{ lat: parseFloat(lat) || 0, lng: parseFloat(lng) || 0 }], null, 0)); };