/* * Apple Map Card Web Component * Licença: NÃO PÚBLICA / USO RESTRITO. * Autoria e titularidade: Sintegra WS. * * Este arquivo não possui licença pública de uso, cópia, redistribuição, * modificação ou revenda. O uso deve ser autorizado expressamente pela Sintegra WS. * * ============================================================ * COMPONENTES * ============================================================ * * 1) * Componente principal do card com Apple MapKit JS, bottom blur, * pins personalizados, POIs da Apple e eventos públicos. * * 2) * Elemento declarativo usado dentro de para criar * pins personalizados no mapa. * * ============================================================ * PARÂMETROS DO * ============================================================ * * token="JWT_MAPKIT" * Obrigatório. * Token JWT do Apple MapKit JS. * * Exemplo: * * * Resultado: * Carrega o MapKit JS e inicializa o mapa. * * * fit="all|selected" * Opcional. Padrão: "all". * * fit="all" * Resultado: * O mapa enquadra todos os pins automaticamente. * * fit="selected" * Resultado: * Quando existe apenas 1 pin, o mapa centraliza no pin selecionado * respeitando zoom/span. Quando existem 2 ou mais pins, o componente * enquadra todos automaticamente e ignora zoom. * * * zoom="1..22|auto" * Opcional. Padrão: "auto". * Usado apenas quando existe 1 pin e fit="selected". * * Exemplo: * * * Resultado: * Aproxima o mapa no pin selecionado quando houver somente 1 pin. * * * span="0.03" * Opcional. Padrão: 0.03. * Fallback usado quando zoom não foi informado ou zoom="auto". * * Exemplo: * * * Resultado: * Controla a região manualmente quando zoom não é usado. * * * callout="true|false" * Opcional. Padrão: false. * Define se os pins personalizados exibem o popup nativo do MapKit. * * Exemplo: * * * Resultado: * O clique no pin atualiza o bottom blur sem abrir o popup nativo. * * * cluster * Opcional. Boolean attribute. * * Exemplo: * * * Resultado: * Pins próximos podem ser agrupados pelo MapKit. * * * poi="all|none|include|exclude" * Opcional. Padrão: "all". * Controla pontos de interesse nativos do Apple Maps. * * poi="all" * Resultado: exibe todos os POIs. * * poi="none" * Resultado: oculta todos os POIs. * * poi="include" * Resultado: mostra apenas categorias declaradas em poi-categories. * * poi="exclude" * Resultado: oculta categorias declaradas em poi-categories. * * * poi-categories="hotel,restaurant,store,cafe" * Opcional. * Lista de categorias separadas por vírgula. * Aceita nomes comuns em inglês e alguns aliases em português. * * Exemplo: * * * Resultado: * Oculta hotéis e restaurantes nativos do mapa. * * * poi-details="none|blur|apple|both" * Opcional. Padrão: "none". * Define o que acontece ao selecionar um POI nativo da Apple. * * poi-details="none" * Resultado: POIs não são selecionáveis. * * poi-details="blur" * Resultado: clique em POI atualiza o bottom blur. * * poi-details="apple" * Resultado: usa o acessório nativo de detalhes da Apple quando disponível. * * poi-details="both" * Resultado: atualiza o bottom blur e usa o acessório nativo da Apple. * * * CSS Custom Properties aceitas no : * --card-radius: raio do card. Padrão: 24px. * --card-footer-height: altura do bottom blur. Padrão: 140px. * --card-blur: intensidade do blur. Padrão: 16px. * --card-text-color: cor do texto. Padrão: #111. * --card-shadow: sombra do card. * * Exemplo: * * * Resultado: * Personaliza o visual do card sem alterar o JS. * * ============================================================ * PARÂMETROS DO * ============================================================ * * pin-id="id-do-pin" * Opcional. Fallback: pin-1, pin-2, ... * Identificador público do pin no evento pin-select. * * latitude="-16.7286" * longitude="-43.8582" * Obrigatórios. * Coordenadas do pin. * * title="Título" * Opcional. * Se ausente ou vazio, o bottom blur não exibe título. * O endereço mantém exatamente a mesma posição visual. * * address="Endereço" * Opcional. Fallback: "Endereço não informado". * Texto exibido no bottom blur. * * icon="URL_DA_IMAGEM" * Opcional. * Quando informado explicitamente, ativa o pin premium com foto. * Quando ausente, usa o SVG padrão definido em DEFAULT_PIN_URL. * * selected * Opcional. Boolean attribute. * Define o pin selecionado inicialmente. Se nenhum for selecionado, * o componente seleciona o primeiro pin. * * callout="true|false" * Opcional. * Sobrescreve o callout do card apenas para aquele pin. * * class="minha-classe" * Opcional. * Classes declaradas no são copiadas para o marcador * renderizado no mapa e também para o pin exibido no bottom blur. * * ============================================================ * EVENTOS PÚBLICOS * ============================================================ * * pin-select * Disparado quando um pin personalizado é selecionado. * * Exemplo: * mapa.addEventListener("pin-select", event => console.log(event.detail)); * * Retorno em event.detail: * { * id, latitude, longitude, title, address, icon, * hasCustomImage, selected, callout, classes, visual * } * * * poi-select * Disparado quando um POI nativo da Apple é selecionado. * * Exemplo: * mapa.addEventListener("poi-select", event => console.log(event.detail)); * * Retorno em event.detail: * { * id, name, address, category, latitude, longitude, * error?, place? * } * * ============================================================ * EXEMPLO MÍNIMO * ============================================================ * *
* * * * *
* * Resultado: * Renderiza o mapa com um pin, sem título visual, mantendo o endereço * alinhado no bottom blur e usando título interno seguro para o MapKit. */ (() => { const MAPKIT_URL = "https://cdn.apple-mapkit.com/mk/6/mapkit.core.js"; const DEFAULT_PIN_URL = "https://sintegraws.com.br/assets/img/pin-place.svg"; const STYLE_ID = "apple-map-card-styles"; let mapKitPromise; function installStyles() { if (document.getElementById(STYLE_ID)) { return; } const style = document.createElement("style"); style.id = STYLE_ID; style.textContent = `apple-map-card { --card-radius: 24px; --card-footer-height: 140px; --card-blur: 16px; --card-text-color: #111; --card-shadow: 0 10px 30px rgb(0 0 0 / 15%); display: block; width: 100%; height: 100%; min-width: 0; min-height: 180px; } apple-map-card, apple-map-card * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; } apple-map-card > apple-map-pin { display: none !important; } apple-map-card > .amc-card { position: relative; width: 100%; height: 100%; overflow: hidden; isolation: isolate; border-radius: var(--card-radius); background: #dfe4ea; box-shadow: var(--card-shadow); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; } apple-map-card .amc-map { position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%; } apple-map-card .amc-footer { position: absolute; right: 0; bottom: 0; left: 0; z-index: 2; height: var(--card-footer-height); pointer-events: none; transform: translateZ(0); -webkit-transform: translateZ(0); } apple-map-card .amc-footer::before { content: ""; position: absolute; inset: 0; z-index: 0; backdrop-filter: blur(var(--card-blur)); -webkit-backdrop-filter: blur(var(--card-blur)); mask-image: linear-gradient(to top, black 0%, black 40%, transparent 100%); -webkit-mask-image: linear-gradient(to top, black 0%, black 40%, transparent 100%); } apple-map-card .amc-content { position: relative; z-index: 1; width: 100%; height: 100%; padding: 25px 20px; display: flex; align-items: flex-end; } apple-map-card .amc-text-info { --footer-pin-column: 40px; width: 100%; min-width: 0; display: grid; grid-template-columns: var(--footer-pin-column) minmax(0, 1fr); align-items: center; gap: 8px; color: var(--card-text-color); } apple-map-card .amc-footer-pin { position: relative; width: 40px; height: 40px; display: block; overflow: visible; justify-self: center; } apple-map-card .amc-footer-pin.amc-photo-pin { width: 54px; height: 60px; display: flex; justify-content: center; align-items: flex-start; transform: scale(0.7407407407); transform-origin: center center; } apple-map-card .amc-copy { min-width: 0; display: grid; gap: 2px; } apple-map-card .amc-title { overflow: hidden; color: inherit; font-size: 15px; font-weight: 700; line-height: 1.25; text-overflow: ellipsis; white-space: nowrap; } apple-map-card .amc-address { color: inherit; font-size: 14px; font-weight: 300; line-height: 1.4; overflow-wrap: anywhere; } apple-map-card .amc-status { position: absolute; inset: 0; z-index: 4; display: grid; place-items: center; padding: 24px; background: rgb(240 242 245 / 92%); color: #333; font: 500 14px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; text-align: center; } apple-map-card .amc-status[hidden] { display: none; } apple-map-card .amc-status[data-error="true"] { color: #b42318; } apple-map-card .amc-map-pin { position: relative; width: 42px; height: 42px; display: block; overflow: visible; cursor: pointer; user-select: none; -webkit-user-select: none; } apple-map-card .amc-map-pin.amc-photo-pin { width: 54px; height: 60px; display: flex; justify-content: center; align-items: flex-start; } apple-map-card .pin-container { position: relative; z-index: 2; width: 100%; height: 100%; transform-origin: bottom center; } apple-map-card .pin-default-image { display: block; width: 42px; height: 42px; object-fit: contain; pointer-events: none; filter: drop-shadow(0 3px 6px rgb(0 0 0 / 28%)); transition: transform 160ms ease, filter 160ms ease; } apple-map-card .amc-map-pin.is-selected:not(.amc-photo-pin) .pin-default-image { transform: translateY(-2px) scale(1.14); filter: drop-shadow(0 5px 9px rgb(0 0 0 / 34%)); } apple-map-card .amc-footer-pin .pin-default-image { width: 40px; height: 40px; filter: drop-shadow(0 2px 6px rgb(0 0 0 / 18%)); transition: none; } apple-map-card .amc-photo-pin .pin-container { position: relative; bottom: 0; z-index: 2; width: 54px; height: 54px; cursor: pointer; transition: filter 0.2s ease; } apple-map-card .amc-map-pin.amc-photo-pin .pin-container:hover { filter: brightness(1.1); } apple-map-card .pin-shadow { position: absolute; left: 50%; bottom: -6px; z-index: 1; width: 34px; height: 12px; margin-left: -17px; background-color: rgba(0, 0, 0, 0.25); border-radius: 50%; filter: blur(3px); transform: scale(1); opacity: 0.5; pointer-events: none; } apple-map-card .pin-shape { width: 54px; height: 54px; padding: 4px; display: flex; justify-content: center; align-items: center; box-sizing: border-box; background-color: #ffffff; border-radius: 50% 50% 50% 6px; transform: rotate(-45deg); box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.15), inset -2px -2px 4px rgba(0, 0, 0, 0.05); } apple-map-card .pin-shape img { width: 100%; height: 100%; border-radius: 50%; transform: rotate(45deg); object-fit: cover; pointer-events: none; } apple-map-card .pin-container.is-bouncing { animation: appleBounce 0.6s cubic-bezier(0.28, 0.84, 0.42, 1) forwards; } apple-map-card .pin-shadow.shadow-bouncing { animation: shadowBounce 0.6s cubic-bezier(0.28, 0.84, 0.42, 1) forwards; } @keyframes appleBounce { 0% { transform: translateY(0) scale(1, 1); } 30% { transform: translateY(-40px) scale(0.9, 1.1); } 50% { transform: translateY(0) scale(1.15, 0.85); } 70% { transform: translateY(-12px) scale(0.97, 1.03); } 85% { transform: translateY(0) scale(1.03, 0.97); } 100% { transform: translateY(0) scale(1, 1); } } @keyframes shadowBounce { 0% { transform: scale(1); opacity: 0.5; } 30% { transform: scale(0.4); opacity: 0.15; } 50% { transform: scale(1.2); opacity: 0.6; } 70% { transform: scale(0.8); opacity: 0.35; } 85% { transform: scale(1.05); opacity: 0.55; } 100% { transform: scale(1); opacity: 0.5; } }`; document.head.appendChild(style); } function loadMapKit(token) { const loaded = window.mapkit?.loadedLibraries || []; if (window.mapkit && loaded.includes("map") && loaded.includes("annotations") && loaded.includes("services")) { return Promise.resolve(window.mapkit); } if (mapKitPromise) { return mapKitPromise; } mapKitPromise = new Promise((resolve, reject) => { const callbackName = "__appleMapCardReady_" + Math.random().toString(36).slice(2); const script = document.createElement("script"); window[callbackName] = () => { delete window[callbackName]; if (window.mapkit) { resolve(window.mapkit); } else { mapKitPromise = null; reject(new Error("O MapKit JS não foi inicializado.")); } }; script.src = MAPKIT_URL; script.async = true; script.crossOrigin = "anonymous"; script.dataset.callback = callbackName; script.dataset.libraries = "map,services,annotations"; script.dataset.token = token; script.addEventListener("error", () => { delete window[callbackName]; mapKitPromise = null; reject(new Error("Não foi possível carregar o MapKit JS.")); }, { once: true }); document.head.appendChild(script); }); return mapKitPromise; } class AppleMapPin extends HTMLElement { static get observedAttributes() { return ["latitude", "longitude", "title", "address", "icon", "selected", "callout", "class"]; } connectedCallback() { this.hidden = true; this._notify(); } attributeChangedCallback() { if (this.isConnected) { this._notify(); } } _notify() { this.dispatchEvent(new CustomEvent("apple-map-pin-change", { bubbles: true })); } } class AppleMapCard extends HTMLElement { static get observedAttributes() { return ["fit", "zoom", "callout", "poi", "poi-categories", "poi-details"]; } constructor() { super(); installStyles(); this._map = null; this._annotations = []; this._selectedAnnotation = null; this._refreshQueued = false; this._poiLookupSequence = 0; this._pinInteractionSequence = 0; this._onPinChange = () => this._queueRefresh(); this._onMapSelect = event => { const annotation = event.annotation; if (annotation?._appleMapCardPin) { const interactionId = annotation._appleMapCardPendingInteractionId || ++this._pinInteractionSequence; this._activatePinAnnotation(annotation, interactionId, true); return; } if (this._isMapFeatureAnnotation(annotation)) { this._handlePOISelection(annotation); } }; this._annotationForMapFeature = annotation => { const mode = this._readPOIDetailsMode(); if (mode === "blur" || mode === "none") { annotation.calloutEnabled = false; annotation.selectionAccessory = null; } }; } connectedCallback() { if (!this._rendered) { this._render(); } this.addEventListener("apple-map-pin-change", this._onPinChange); this._childrenObserver = new MutationObserver(records => { const changed = records.some(record => [...record.addedNodes, ...record.removedNodes].some(node => node.nodeType === Node.ELEMENT_NODE && node.matches?.("apple-map-pin"))); if (changed) { this._queueRefresh(); } }); this._childrenObserver.observe(this, { childList: true }); this._start(); } disconnectedCallback() { this.removeEventListener("apple-map-pin-change", this._onPinChange); this._childrenObserver?.disconnect(); this._resizeObserver?.disconnect(); cancelAnimationFrame(this._fitFrame1); cancelAnimationFrame(this._fitFrame2); cancelAnimationFrame(this._poiConfigFrame); if (this._map) { this._map.removeEventListener?.("select", this._onMapSelect); this._map.destroy?.(); } this._map = null; this._annotations = []; this._selectedAnnotation = null; } attributeChangedCallback(name, oldValue, newValue) { if (oldValue === newValue || !this.isConnected) { return; } if (["poi", "poi-categories", "poi-details"].includes(name)) { this._schedulePOIConfiguration(); return; } if (name === "callout") { this._queueRefresh(); return; } if (name === "fit" || name === "zoom") { this._scheduleFit(false); } } _render() { const card = document.createElement("div"); card.className = "amc-card"; card.innerHTML = `
Carregando mapa…
`; this.prepend(card); this._mapElement = card.querySelector(".amc-map"); this._footer = card.querySelector(".amc-footer"); this._textInfo = card.querySelector(".amc-text-info"); this._footerVisual = card.querySelector(".amc-footer-pin"); this._titleElement = card.querySelector(".amc-title"); this._addressElement = card.querySelector(".amc-address"); this._statusElement = card.querySelector(".amc-status"); this._renderPinVisual(this._footerVisual, DEFAULT_PIN_URL, [], false); this._rendered = true; } async _start() { const token = this.getAttribute("token")?.trim(); if (!token) { this._showStatus('Informe o token no atributo "token" do .', true); return; } this._showStatus("Carregando mapa…"); try { const mapkit = await loadMapKit(token); if (!this.isConnected || this._map) { return; } const options = { selectableMapFeatures: this._createSelectableMapFeatures(mapkit), annotationForMapFeature: this._annotationForMapFeature }; const filter = this._createPOIFilter(mapkit); const accessory = this._createPOISelectionAccessory(mapkit); if (filter) { options.pointOfInterestFilter = filter; } if (accessory) { options.selectableMapFeatureSelectionAccessory = accessory; } this._map = new mapkit.Map(this._mapElement, options); this._placeLookup = new mapkit.PlaceLookup(); this._map.addEventListener("select", this._onMapSelect); this._resizeObserver = new ResizeObserver(() => { if (this._annotations.length) { this._scheduleFit(false); } }); this._resizeObserver.observe(this); this._rebuildAnnotations(); this._hideStatus(); } catch (error) { console.error(error); this._showStatus("Não foi possível carregar o mapa. Verifique o token e o domínio autorizado.", true); } } _queueRefresh() { if (this._refreshQueued) { return; } this._refreshQueued = true; queueMicrotask(() => { this._refreshQueued = false; this._rebuildAnnotations(); }); } _scheduleFit(animate = true) { cancelAnimationFrame(this._fitFrame1); cancelAnimationFrame(this._fitFrame2); this._fitFrame1 = requestAnimationFrame(() => { this._fitFrame2 = requestAnimationFrame(() => this._fitAnnotations(animate)); }); } _schedulePOIConfiguration() { cancelAnimationFrame(this._poiConfigFrame); this._poiConfigFrame = requestAnimationFrame(() => this._applyPOIConfiguration()); } _booleanAttribute(element, name, fallback) { if (!element.hasAttribute(name)) { return fallback; } const value = element.getAttribute(name).trim().toLowerCase(); return !["false", "0", "off", "no"].includes(value); } _readPins() { const defaultCallout = this._booleanAttribute(this, "callout", false); return [...this.querySelectorAll(":scope > apple-map-pin")].map((element, index) => { const latitude = Number(element.getAttribute("latitude")); const longitude = Number(element.getAttribute("longitude")); if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) { console.warn("Pin ignorado por coordenadas inválidas:", element); return null; } const iconAttribute = element.getAttribute("icon"); const hasCustomImage = typeof iconAttribute === "string" && iconAttribute.trim() !== ""; return { id: element.getAttribute("pin-id") || `pin-${index + 1}`, latitude, longitude, title: element.getAttribute("title")?.trim() || "", address: element.getAttribute("address") || "Endereço não informado", icon: hasCustomImage ? iconAttribute.trim() : DEFAULT_PIN_URL, hasCustomImage, selected: element.hasAttribute("selected"), callout: this._booleanAttribute(element, "callout", defaultCallout), classes: [...element.classList], visual: null }; }).filter(Boolean); } _rebuildAnnotations() { if (!this._map || !window.mapkit) { return; } if (this._annotations.length) { this._map.removeAnnotations(this._annotations); } const pins = this._readPins(); this._annotations = []; this._selectedAnnotation = null; if (!pins.length) { this._showStatus("Adicione pelo menos um ao componente.", true); return; } const shouldCluster = this.hasAttribute("cluster"); for (const pin of pins) { const coordinate = new mapkit.Coordinate(pin.latitude, pin.longitude); const annotationTitle = pin.title || pin.address || pin.id; let annotation; const factory = () => { const marker = document.createElement("div"); marker.className = "amc-map-pin"; if (pin.classes.length) { marker.classList.add(...pin.classes); } marker.setAttribute("role", "button"); marker.setAttribute("tabindex", "0"); marker.setAttribute("aria-label", pin.title ? `${pin.title}. ${pin.address}` : pin.address); const visual = this._renderPinVisual(marker, pin.icon, pin.classes, pin.hasCustomImage); pin.visual = visual; const beginInteraction = () => { if (!annotation) { return null; } const interactionId = ++this._pinInteractionSequence; annotation._appleMapCardPendingInteractionId = interactionId; clearTimeout(annotation._appleMapCardInteractionTimer); annotation._appleMapCardInteractionTimer = setTimeout(() => { if (annotation._appleMapCardPendingInteractionId === interactionId) { annotation._appleMapCardPendingInteractionId = null; } }, 1500); return interactionId; }; marker.addEventListener("pointerdown", beginInteraction); marker.addEventListener("click", () => { if (!annotation) { return; } const interactionId = annotation._appleMapCardPendingInteractionId || beginInteraction(); this._activatePinAnnotation(annotation, interactionId, true); }); marker.addEventListener("keydown", event => { if (event.key !== "Enter" && event.key !== " ") { return; } event.preventDefault(); const interactionId = beginInteraction(); this._activatePinAnnotation(annotation, interactionId, true); }); return marker; }; const options = { title: annotationTitle, subtitle: pin.address, calloutEnabled: pin.callout, anchorOffset: new DOMPoint(0, pin.hasCustomImage ? -27 : -21) }; if (shouldCluster) { options.clusteringIdentifier = "apple-map-card-pins"; } annotation = new mapkit.Annotation(coordinate, factory, options); annotation._appleMapCardPin = pin; this._annotations.push(annotation); } this._map.addAnnotations(this._annotations); const initial = this._annotations.find(item => item._appleMapCardPin.selected) || this._annotations[0]; this._selectAnnotation(initial, false); this._scheduleFit(false); this._hideStatus(); } _renderPinVisual(target, icon, classes = [], hasCustomImage = false) { target.replaceChildren(); target.classList.remove("amc-photo-pin"); for (const className of classes) { if (className !== "amc-photo-pin") { target.classList.add(className); } } const container = document.createElement("div"); container.className = "pin-container"; const image = document.createElement("img"); image.src = icon; image.alt = ""; image.draggable = false; let shadow = null; if (hasCustomImage) { target.classList.add("amc-photo-pin"); shadow = document.createElement("div"); shadow.className = "pin-shadow"; const shape = document.createElement("div"); shape.className = "pin-shape"; shape.appendChild(image); container.appendChild(shape); target.append(shadow, container); } else { image.className = "pin-default-image"; container.appendChild(image); target.appendChild(container); } return { shadow, container }; } _activatePinAnnotation(annotation, interactionId, emitEvent) { if (!annotation?._appleMapCardPin || !interactionId) { return; } const now = performance.now(); if (annotation._appleMapCardHandledInteractionId === interactionId || (annotation._appleMapCardLastActivationAt && now - annotation._appleMapCardLastActivationAt < 80)) { return; } annotation._appleMapCardHandledInteractionId = interactionId; annotation._appleMapCardLastActivationAt = now; this._animatePin(annotation._appleMapCardPin); this._selectAnnotation(annotation, emitEvent); } _animatePin(pin) { const visual = pin?.visual; if (!visual?.container) { return; } visual.container.classList.remove("is-bouncing"); visual.shadow?.classList.remove("shadow-bouncing"); void visual.container.offsetWidth; visual.container.classList.add("is-bouncing"); visual.shadow?.classList.add("shadow-bouncing"); } _readZoom() { const raw = this.getAttribute("zoom"); if (raw === null || raw.trim() === "" || raw.trim().toLowerCase() === "auto") { return null; } const zoom = Number(raw); return Number.isFinite(zoom) ? Math.min(Math.max(zoom, 1), 22) : null; } _spanFromZoom(latitude, zoom) { const width = Math.max(this._mapElement.clientWidth, 256); const height = Math.max(this._mapElement.clientHeight, 256); const worldSize = 256 * (2 ** zoom); const limitedLatitude = Math.min(Math.max(latitude, -85.05112878), 85.05112878); const latitudeSin = Math.sin(limitedLatitude * Math.PI / 180); const centerY = (0.5 - Math.log((1 + latitudeSin) / (1 - latitudeSin)) / (4 * Math.PI)) * worldSize; const northY = Math.max(centerY - height / 2, 0); const southY = Math.min(centerY + height / 2, worldSize); const toLatitude = y => { const mercator = Math.PI - (2 * Math.PI * y) / worldSize; return (180 / Math.PI) * Math.atan(Math.sinh(mercator)); }; return { latitudeDelta: Math.max(toLatitude(northY) - toLatitude(southY), 0.000001), longitudeDelta: Math.max(Math.min(360 * width / worldSize, 360), 0.000001) }; } _fitAnnotations(animate = true) { if (!this._map || !this._annotations.length) { return; } const padding = new mapkit.Padding(28, 28, this._footer.offsetHeight + 28, 28); if (this._annotations.length > 1 || (this.getAttribute("fit") || "all") === "all") { this._map.showItems(this._annotations, { animate, padding }); return; } const pin = this._selectedAnnotation?._appleMapCardPin; if (!pin) { return; } const zoom = this._readZoom(); const span = zoom === null ? { latitudeDelta: Math.max(Number(this.getAttribute("span")) || 0.03, 0.0001), longitudeDelta: Math.max(Number(this.getAttribute("span")) || 0.03, 0.0001) } : this._spanFromZoom(pin.latitude, zoom); const region = new mapkit.CoordinateRegion( new mapkit.Coordinate(pin.latitude, pin.longitude), new mapkit.CoordinateSpan(span.latitudeDelta, span.longitudeDelta) ); if (typeof this._map.setRegionAnimated === "function") { this._map.setRegionAnimated(region, animate); } else { this._map.region = region; } } _selectAnnotation(annotation, emitEvent) { if (!annotation?._appleMapCardPin) { return; } this._poiLookupSequence += 1; this._selectedAnnotation = annotation; const pin = annotation._appleMapCardPin; this._setFooter(pin.title, pin.address, pin.icon, pin.classes, pin.hasCustomImage); this._mapElement.setAttribute("aria-label", pin.title ? `Mapa de ${pin.title}. ${pin.address}` : `Mapa de ${pin.address}`); requestAnimationFrame(() => { for (const item of this._annotations) { item.element?.classList.toggle("is-selected", item === annotation); } }); if (emitEvent && this._annotations.length === 1 && (this.getAttribute("fit") || "all") === "selected") { this._scheduleFit(true); } if (emitEvent) { this.dispatchEvent(new CustomEvent("pin-select", { bubbles: true, detail: { ...pin } })); } } _setFooter(title, address, icon = DEFAULT_PIN_URL, classes = [], hasCustomImage = false) { const hasTitle = String(title || "").trim() !== ""; this._titleElement.textContent = hasTitle ? title : "\u00A0"; this._addressElement.textContent = address; this._footerVisual.style.top = hasTitle ? "" : "10px"; this._textInfo.style.setProperty("--footer-pin-column", "40px"); this._footerVisual.className = "amc-footer-pin"; this._renderPinVisual(this._footerVisual, icon, classes, hasCustomImage); } _readPOIMode() { const mode = (this.getAttribute("poi") || "all").trim().toLowerCase(); return ["all", "none", "include", "exclude"].includes(mode) ? mode : "all"; } _readPOIDetailsMode() { const mode = (this.getAttribute("poi-details") || "none").trim().toLowerCase(); return ["none", "blur", "apple", "both"].includes(mode) ? mode : "none"; } _normalizeName(value) { return String(value) .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .replace(/[^a-z0-9]/gi, "") .toLowerCase(); } _readPOICategories(mapkit) { const requested = (this.getAttribute("poi-categories") || "") .split(",") .map(value => value.trim()) .filter(Boolean); const source = mapkit.PointOfInterestCategory || {}; const index = new Map(); for (const [key, value] of Object.entries(source)) { index.set(this._normalizeName(key), value); index.set(this._normalizeName(value), value); } const aliases = { restaurant: "Restaurant", restaurante: "Restaurant", hotel: "Hotel", store: "Store", loja: "Store", cafe: "Cafe", cafeteria: "Cafe", bakery: "Bakery", padaria: "Bakery", bank: "Bank", banco: "Bank", pharmacy: "Pharmacy", farmacia: "Pharmacy", hospital: "Hospital", parking: "Parking", estacionamento: "Parking", gasstation: "GasStation", posto: "GasStation", postodecombustivel: "GasStation", museum: "Museum", museu: "Museum", park: "Park", parque: "Park" }; const categories = requested.map(value => { const normalized = this._normalizeName(value); const canonical = aliases[normalized] || value; return ( index.get(normalized) || index.get(this._normalizeName(canonical)) || source[canonical] || null ); }).filter(Boolean); return [...new Set(categories)]; } _staticFilter(Filter, name) { const value = Filter?.[name]; return typeof value === "function" ? value.call(Filter) : value; } _createPOIFilter(mapkit) { const Filter = mapkit.PointOfInterestFilter; if (!Filter) { return null; } const mode = this._readPOIMode(); const categories = this._readPOICategories(mapkit); if (mode === "none") { return this._staticFilter(Filter, "excludingAllCategories"); } if (mode === "include") { return categories.length ? Filter.including(categories) : this._staticFilter(Filter, "excludingAllCategories"); } if (mode === "exclude") { return categories.length ? Filter.excluding(categories) : this._staticFilter(Filter, "includingAllCategories"); } return this._staticFilter(Filter, "includingAllCategories"); } _createSelectableMapFeatures(mapkit) { const pointOfInterest = mapkit.MapFeatureType?.PointOfInterest; if (this._readPOIDetailsMode() === "none" || this._readPOIMode() === "none" || !pointOfInterest) { return []; } return [pointOfInterest]; } _createPOISelectionAccessory(mapkit) { const mode = this._readPOIDetailsMode(); if (!mapkit.PlaceSelectionAccessory || (mode !== "apple" && mode !== "both")) { return null; } return new mapkit.PlaceSelectionAccessory(); } _applyPOIConfiguration() { if (!this._map || !window.mapkit) { return; } this._poiLookupSequence += 1; const filter = this._createPOIFilter(window.mapkit); if (filter) { this._map.pointOfInterestFilter = filter; } this._map.annotationForMapFeature = this._annotationForMapFeature; this._map.selectableMapFeatureSelectionAccessory = this._createPOISelectionAccessory(window.mapkit); const features = this._createSelectableMapFeatures(window.mapkit); this._map.selectableMapFeatures = []; requestAnimationFrame(() => { if (this._map) { this._map.selectableMapFeatures = features; } }); } _isMapFeatureAnnotation(annotation) { if (!annotation || annotation._appleMapCardPin) { return false; } const Type = window.mapkit?.MapFeatureAnnotation; if (Type && annotation instanceof Type) { return true; } return Boolean( annotation.featureType || annotation.category || annotation.pointOfInterestCategory ); } async _handlePOISelection(annotation) { const mode = this._readPOIDetailsMode(); if (mode === "none") { return; } const sequence = ++this._poiLookupSequence; const provisional = this._normalizePOIData(annotation, null); if (mode === "blur" || mode === "both") { this._setFooter(provisional.name, provisional.address); } let place = null; let errorMessage = null; if (annotation.id && this._placeLookup) { try { place = await this._placeLookup.getPlace(annotation.id); } catch (error) { console.warn("Não foi possível consultar o estabelecimento:", error); errorMessage = "A Apple não retornou os detalhes completos deste local."; } } if (sequence !== this._poiLookupSequence || !this.isConnected) { return; } const data = this._normalizePOIData(annotation, place); if (errorMessage) { data.error = errorMessage; } if (mode === "blur" || mode === "both") { this._setFooter(data.name, data.address); this._mapElement.setAttribute("aria-label", `Mapa de ${data.name}. ${data.address}`); } this.dispatchEvent(new CustomEvent("poi-select", { bubbles: true, detail: { ...data, place } })); } _normalizePOIData(annotation, place) { const coordinate = place?.coordinate || annotation?.coordinate || null; const latitude = Number(coordinate?.latitude); const longitude = Number(coordinate?.longitude); const category = place?.pointOfInterestCategory || annotation?.pointOfInterestCategory || annotation?.category || ""; return { id: place?.id || annotation?.id || "", name: place?.name || annotation?.title || "Ponto de interesse", address: place?.formattedAddress || annotation?.subtitle || "Endereço não informado", category: category ? String(category) : "Não informada", latitude: Number.isFinite(latitude) ? latitude : null, longitude: Number.isFinite(longitude) ? longitude : null }; } _showStatus(message, error = false) { this._statusElement.textContent = message; this._statusElement.dataset.error = String(error); this._statusElement.hidden = false; } _hideStatus() { this._statusElement.hidden = true; } } if (!customElements.get("apple-map-pin")) { customElements.define("apple-map-pin", AppleMapPin); } if (!customElements.get("apple-map-card")) { customElements.define("apple-map-card", AppleMapCard); } })();