UNPKG

@photo-sphere-viewer/map-plugin

Version:

Photo Sphere Viewer plugin to add a minimap with the panorama location.

1,298 lines (1,271 loc) 46 kB
/*! * Photo Sphere Viewer / Map Plugin 5.14.0 * @copyright 2015-2025 Damien "Mistic" Sorel * @licence MIT (https://opensource.org/licenses/MIT) */ var __defProp = Object.defineProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/index.ts import { DEFAULTS } from "@photo-sphere-viewer/core"; // src/events.ts var events_exports = {}; __export(events_exports, { SelectHotspot: () => SelectHotspot, ViewChanged: () => ViewChanged }); import { TypedEvent } from "@photo-sphere-viewer/core"; var _SelectHotspot = class _SelectHotspot extends TypedEvent { /** @internal */ constructor(hotspotId) { super(_SelectHotspot.type); this.hotspotId = hotspotId; } }; _SelectHotspot.type = "select-hotspot"; var SelectHotspot = _SelectHotspot; var _ViewChanged = class _ViewChanged extends TypedEvent { /** @internal */ constructor(view) { super(_ViewChanged.type); this.view = view; } }; _ViewChanged.type = "view-changed"; var ViewChanged = _ViewChanged; // src/MapPlugin.ts import { AbstractConfigurablePlugin, events as events2, utils as utils3 } from "@photo-sphere-viewer/core"; import { Color } from "three"; // src/components/MapComponent.ts import { AbstractComponent as AbstractComponent3, CONSTANTS as CONSTANTS2, events, SYSTEM as SYSTEM2, utils as utils2 } from "@photo-sphere-viewer/core"; import { MathUtils } from "three"; // src/constants.ts var MARKER_DATA_KEY = "map"; var HOTSPOT_GENERATED_ID = "__generated__"; var HOTSPOT_MARKER_ID = "__marker__"; var PIN_SHADOW_OFFSET = 2; var PIN_SHADOW_BLUR = 4; var MAP_SHADOW_BLUR = 10; // src/utils.ts import { SYSTEM } from "@photo-sphere-viewer/core"; function loadImage(src) { const image = document.createElement("img"); if (!src.includes("<svg")) { image.src = src; } else { if (!/<svg[^>]*width="/.test(src) && src.includes("viewBox")) { const [, , , width, height] = /viewBox="([0-9-]+) ([0-9-]+) ([0-9]+) ([0-9]+)"/.exec(src); src = src.replace("<svg", `<svg width="${width}px" height="${height}px"`); } const src64 = `data:image/svg+xml;base64,${window.btoa(src)}`; image.src = src64; } return image; } function getImageHtml(src) { if (!src) { return ""; } else if (!src.includes("<svg")) { return `<img src="${src}">`; } else { return src; } } function getStyle(defaultStyle, style, isHover) { return { image: isHover ? style.hoverImage ?? style.image ?? defaultStyle.hoverImage ?? defaultStyle.image : style.image ?? defaultStyle.image, size: isHover ? style.hoverSize ?? style.size ?? defaultStyle.hoverSize ?? defaultStyle.size : style.size ?? defaultStyle.size, color: isHover ? style.hoverColor ?? style.color ?? defaultStyle.hoverColor ?? defaultStyle.color : style.color ?? defaultStyle.color, borderColor: isHover ? style.hoverBorderColor ?? style.borderColor ?? defaultStyle.hoverBorderColor ?? defaultStyle.borderColor : style.borderColor ?? defaultStyle.borderColor, borderSize: isHover ? style.hoverBorderSize ?? style.borderSize ?? defaultStyle.hoverBorderSize ?? defaultStyle.borderSize : style.borderSize ?? defaultStyle.borderSize }; } function unprojectPoint(pt, yaw, zoom) { return { x: (Math.cos(yaw) * pt.x - Math.sin(yaw) * pt.y) / zoom, y: (Math.sin(yaw) * pt.x + Math.cos(yaw) * pt.y) / zoom }; } function projectPoint(pt, yaw, zoom) { return { x: (Math.cos(-yaw) * pt.x - Math.sin(-yaw) * pt.y) * zoom, y: (Math.sin(-yaw) * pt.x + Math.cos(-yaw) * pt.y) * zoom }; } function canvasShadow(context, offsetX, offsetY, blur, color = "black") { context.shadowOffsetX = offsetX * SYSTEM.pixelRatio; context.shadowOffsetY = offsetY * SYSTEM.pixelRatio; context.shadowBlur = blur * SYSTEM.pixelRatio; context.shadowColor = color; } function drawImageCentered(context, image, size) { const w = image.width; const h = image.height; drawImageHighDpi( context, image, -size / 2, -(h / w * size) / 2, size, h / w * size ); } function drawImageHighDpi(context, image, x, y, w, h) { context.drawImage( image, 0, 0, image.width, image.height, x * SYSTEM.pixelRatio, y * SYSTEM.pixelRatio, w * SYSTEM.pixelRatio, h * SYSTEM.pixelRatio ); } function rgbToRgba(rgb, alpha) { return `rgba(${rgb.slice(4, -1)},${alpha})`; } // src/components/MapCloseButton.ts import { CONSTANTS } from "@photo-sphere-viewer/core"; // src/icons/map.svg var map_default = '<svg viewBox="114 45 472 472" xmlns="http://www.w3.org/2000/svg"><g fill="currentColor"><path d="M383.6 196a67.3 67.3 0 1 0-134.5.1 67.3 67.3 0 0 0 134.5-.1zm-100.8 0a33.6 33.6 0 1 1 67.3 0 33.6 33.6 0 0 1-67.3 0z"/><path d="M584 340.8a16.8 16.8 0 0 0-15.6-10.4H403.8c25.2-40.2 47-88 47-133.4A135 135 0 0 0 316.4 61.6 135 135 0 0 0 182 197c0 55.8 33 115.3 64.7 159.8L120.4 469a16.8 16.8 0 0 0 11.2 29.4H434c4.5 0 8.7-1.8 11.9-5l134.4-134.3c4.8-4.8 6.2-12 3.6-18.3zM215.5 197c0-56.1 45.2-101.8 100.8-101.8 55.6 0 100.8 45.6 100.8 101.8 0 65-57.1 144.2-100.8 192.8C273 341.7 215.6 262.3 215.6 197zM427 464.8H175.8l91.3-81.1a575.6 575.6 0 0 0 37.4 42.6 16.8 16.8 0 0 0 23.8 0c2.2-2.2 26.3-26.7 52.6-62.3h147z"/></g><!-- Created by Ayub Irawan from Noun Project --></svg>'; // src/components/AbstractMapButton.ts import { AbstractComponent } from "@photo-sphere-viewer/core"; var INVERT_POSITIONS = { top: "bottom", bottom: "top", left: "right", right: "left" }; function getButtonPosition(mapPosition, direction) { switch (direction) { case 1 /* DIAGONAL */: return [INVERT_POSITIONS[mapPosition[0]], INVERT_POSITIONS[mapPosition[1]]]; case 2 /* HORIZONTAL */: return [mapPosition[0], INVERT_POSITIONS[mapPosition[1]]]; case 3 /* VERTICAL */: return [INVERT_POSITIONS[mapPosition[0]], mapPosition[1]]; default: return mapPosition; } } var AbstractMapButton = class extends AbstractComponent { constructor(map, position) { super(map, {}); this.map = map; this.position = position; } applyConfig() { this.container.className = `psv-map__button psv-map__button--${getButtonPosition(this.map.config.position, this.position).join("-")}`; this.update(); } // eslint-disable-next-line @typescript-eslint/no-empty-function update() { } }; // src/components/MapCloseButton.ts var MapCloseButton = class extends AbstractMapButton { constructor(map) { super(map, 0 /* DEFAULT */); this.container.addEventListener("click", (e) => { map.toggleCollapse(); e.stopPropagation(); }); } applyConfig() { super.applyConfig(); this.container.classList.add("psv-map__button-close"); } update() { this.container.innerHTML = this.map.collapsed ? map_default : CONSTANTS.ICONS.close; this.container.title = this.map.collapsed ? this.viewer.config.lang["map"] : this.viewer.config.lang.close; } }; // src/icons/compass.svg var compass_default = '<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path d="M50,0L70,50L50,100L30,50Z M50,86L64,50L36,50Z" fill="currentColor"/></svg>'; // src/components/MapCompassButton.ts var MapCompassButton = class extends AbstractMapButton { constructor(map) { super(map, 3 /* VERTICAL */); this.container.innerHTML = compass_default; this.container.querySelector("svg").style.width = "80%"; this.container.addEventListener("click", (e) => { this.viewer.dynamics.position.goto({ yaw: -map.config.rotation }, 2); e.stopPropagation(); }); } rotate(angle) { this.container.querySelector("svg").style.transform = `rotate3d(0, 0, 1, ${-angle}rad)`; } update() { this.container.title = this.viewer.config.lang["mapNorth"]; } }; // src/icons/maximize.svg var maximize_default = '<svg viewBox="95 25 510 510" xmlns="http://www.w3.org/2000/svg"><path d="M604.2 39.8v481c0 7.8-6.1 14-14 14H358.4c-7.8 0-14-6.2-14-14s6.2-14 14-14h217.8v-453H123.8v216.7c0 7.8-6.2 14-14 14-7.9 0-14-6.2-14-14V39.8c0-7.9 6.1-14 14-14h481c7.3 0 13.4 6.1 13.4 14zm-304 304v176.4c0 7.9-6.2 14-14 14H109.8c-7.9 0-14-6.1-14-14V343.8c0-7.8 6.1-14 14-14h176.4c7.2 0 14 6.8 14 14zm-28 14H123.8v148.4h148.4zm215.6-195.4v79.5c0 7.9 6.1 14 14 14 7.8 0 14-6.1 14-14V128.2c0-7.8-6.2-14-14-14H388.6c-7.8 0-14 6.2-14 14 0 7.9 6.2 14 14 14h79L326.5 283.4a13.5 13.5 0 0 0 0 19.6c2.8 2.8 6.1 3.9 10 3.9 4 0 7.3-1.1 10.1-4z" fill="currentColor"/><!-- Created by Gregor Cresnar from Noun Project --></svg>'; // src/icons/minimize.svg var minimize_default = '<svg viewBox="95 25 510 510" xmlns="http://www.w3.org/2000/svg"><path d="M109.8 25.8h481c7.8 0 14 6.1 14 14v481c0 7.8-6.2 14-14 14H358.4c-7.8 0-14-6.2-14-14s6.2-14 14-14h217.8v-453H123.8v216.7c0 7.8-6.2 14-14 14-7.9 0-14-6.2-14-14V39.8c0-7.9 6.1-14 14-14zm176.4 508.4H109.8c-7.9 0-14-6.1-14-14V343.8c0-7.8 6.1-14 14-14h176.4c7.8 0 14 6.2 14 14v176.4c0 7.9-6.8 14-14 14zm-14-176.4H123.8v148.4h148.4zm64.4-191.5c-7.9 0-14 6.2-14 14v113.1c0 7.9 6.1 14 14 14h113c8 0 14-6.1 14-14s-6-14-14-14h-79.4l141-141a13.5 13.5 0 0 0 0-19.7 13.5 13.5 0 0 0-19.5 0L350.6 259.8v-79.5c0-7.8-6.2-14-14-14z" fill="currentColor"/><!-- Created by Gregor Cresnar from Noun Project --></svg>'; // src/components/MapMaximizeButton.ts var ROTATION = { "bottom-left": 0, "bottom-right": -90, "top-right": 180, "top-left": 90 }; var MapMaximizeButton = class extends AbstractMapButton { constructor(map) { super(map, 1 /* DIAGONAL */); this.container.addEventListener("click", (e) => { map.toggleMaximized(); e.stopPropagation(); }); } update() { this.container.innerHTML = this.map.maximized ? minimize_default : maximize_default; this.container.querySelector("svg").style.transform = `rotate3d(0, 0, 1, ${ROTATION[this.map.config.position.join("-")]}deg)`; this.container.title = this.map.maximized ? this.viewer.config.lang["mapMinimize"] : this.viewer.config.lang["mapMaximize"]; } }; // src/icons/reset.svg var reset_default = '<svg viewBox="170 100 360 360" xmlns="http://www.w3.org/2000/svg"><g fill="currentColor"><path d="M518.6 269h-18.5a150.8 150.8 0 0 0-138-137.9v-20.9c0-5.8-4.7-10.6-10.5-10.6h-3.2c-5.8 0-10.6 4.8-10.6 10.6v21A150.8 150.8 0 0 0 200 269h-18.5c-5.9 0-10.6 4.7-10.6 10.6v3.2c0 5.8 4.7 10.5 10.6 10.5h18.5c6 73.4 64.6 132 138 138v18.5c0 5.8 4.7 10.6 10.5 10.6h3.2c5.8 0 10.6-4.8 10.6-10.6v-18.6c73.3-5.9 132-64.5 137.9-137.9h18.5c5.9 0 10.6-4.7 10.6-10.5v-3.2c0-5.9-4.7-10.6-10.6-10.6zM362.2 414.4v-9.8c0-5.9-4.8-10.6-10.6-10.6h-3.2c-5.8 0-10.6 4.7-10.6 10.6v9.8a134 134 0 0 1-121-121h9.8c5.9 0 10.6-4.8 10.6-10.6v-3.2c0-5.9-4.7-10.6-10.6-10.6h-9.8a134 134 0 0 1 121-121v7.5c0 5.8 4.8 10.5 10.6 10.5h3.2c5.8 0 10.6-4.7 10.6-10.5V148a134 134 0 0 1 121 121h-9.8c-5.9 0-10.6 4.7-10.6 10.6v3.2c0 5.8 4.7 10.5 10.6 10.5h9.8a134 134 0 0 1-121 121z"/><path d="M355.4 222a6 6 0 0 0-10.7 0L291 320a8.3 8.3 0 0 0 9.7 12l39.2-11.7c6.6-2 13.6-2 20.2 0l39.2 11.7a8.3 8.3 0 0 0 9.7-12z"/></g><!-- Created by muhammad benani from Noun Project --></svg>'; // src/components/MapResetButton.ts var MapResetButton = class extends AbstractMapButton { constructor(map) { super(map, 2 /* HORIZONTAL */); this.container.innerHTML = reset_default; this.container.querySelector("svg").style.width = "80%"; this.container.addEventListener("click", (e) => { map.reset(); e.stopPropagation(); }); } update() { this.container.title = this.viewer.config.lang["mapReset"]; } }; // src/components/MapZoomToolbar.ts import { AbstractComponent as AbstractComponent2, utils } from "@photo-sphere-viewer/core"; // src/icons/minus.svg var minus_default = '<svg viewBox="128 58 444 444" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M350 58.2a221.8 221.8 0 1 0 0 443.6 221.8 221.8 0 0 0 0-443.6zm130.3 252.7H219.7a31 31 0 1 1 0-61.8h260.6a31 31 0 1 1 0 61.8z"/><!-- Created by Iconika from Noun Project --></svg>'; // src/icons/plus.svg var plus_default = '<svg viewBox="143.8 73.8 412.5 412.5" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M350 73.8a206.2 206.2 0 1 0 0 412.4 206.2 206.2 0 0 0 0-412.4zm117.3 234H378v89.5a27.9 27.9 0 1 1-55.8 0V308h-89.4a27.9 27.9 0 1 1 0-55.8H322v-89.5a27.9 27.9 0 1 1 55.8 0v89.5h89.5a27.9 27.9 0 1 1 0 55.8z"/><!-- Created by Iconika from Noun Project --></svg>'; // src/components/MapZoomToolbar.ts var MapZoomToolbar = class extends AbstractComponent2 { constructor(map) { super(map, { className: "psv-map__toolbar" }); this.map = map; this.handler = new utils.PressHandler(100); this.container.innerHTML = `${minus_default}<span class="psv-map__toolbar-text">100%</span>${plus_default}`; this.zoomIndicator = this.container.querySelector(".psv-map__toolbar-text"); const zoomButtons = this.container.querySelectorAll("svg"); zoomButtons[0].dataset["delta"] = "-1"; zoomButtons[1].dataset["delta"] = "1"; this.container.addEventListener("mousedown", this); window.addEventListener("mouseup", this); this.container.addEventListener("touchstart", this); window.addEventListener("touchend", this); } destroy() { window.removeEventListener("mouseup", this); window.removeEventListener("touchend", this); super.destroy(); } handleEvent(e) { switch (e.type) { case "mousedown": case "touchstart": { const button = utils.getMatchingTarget(e, "svg[data-delta]"); const delta = button?.dataset["delta"]; if (delta) { cancelAnimationFrame(this.animation); this.handler.down(); this.time = performance.now(); this.animateZoom(parseInt(delta, 10)); e.preventDefault(); e.stopPropagation(); } break; } case "mouseup": case "touchend": if (this.animation) { this.handler.up(() => { cancelAnimationFrame(this.animation); this.animation = null; }); e.preventDefault(); e.stopPropagation(); } break; default: break; } } setText(zoom) { this.zoomIndicator.innerText = `${Math.round(Math.exp(zoom) * 100)}%`; } animateZoom(delta) { this.animation = requestAnimationFrame((t) => { this.map.zoom(delta * (t - this.time) / 1e3); this.time = t; this.animateZoom(delta); }); } }; // src/overlay-round.svg var overlay_round_default = '<svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">\n <defs>\n <radialGradient id="gradient">\n <stop offset="80%" stop-color="rgba(255, 255, 255, 0)"/>\n <stop offset="90%" stop-color="rgba(255, 255, 255, .5)"/>\n </radialGradient>\n </defs>\n <circle cx="250" cy="250" r="250" fill="url(#gradient)"/>\n <circle cx="250" cy="250" r="245" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="10"/>\n <g fill="#222">\n <rect x="1" y="248" width="18" height="4"/>\n <rect x="481" y="248" width="18" height="4"/>\n <rect x="248" y="1" width="4" height="18"/>\n <rect x="248" y="481" width="4" height="18"/>\n </g>\n</svg>'; // src/overlay-square.svg var overlay_square_default = '<svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">\n <defs>\n <linearGradient id="gradient">\n <stop offset="5%" stop-color="rgba(255, 255, 255, .5)"/>\n <stop offset="10%" stop-color="rgba(255, 255, 255, 0)"/>\n <stop offset="90%" stop-color="rgba(255, 255, 255, 0)"/>\n <stop offset="95%" stop-color="rgba(255, 255, 255, .5)"/>\n </linearGradient>\n <linearGradient id="gradient2" x1="0" x2="0" y1="0" y2="1">\n <stop offset="5%" stop-color="rgba(255, 255, 255, .5)"/>\n <stop offset="10%" stop-color="rgba(255, 255, 255, 0)"/>\n <stop offset="90%" stop-color="rgba(255, 255, 255, 0)"/>\n <stop offset="95%" stop-color="rgba(255, 255, 255, .5)"/>\n </linearGradient>\n </defs>=\n <rect x="0" y="0" width="500" height="500" fill="url(#gradient)"/>\n <rect x="0" y="0" width="500" height="500" fill="url(#gradient2)"/>\n <rect x="5" y="5" width="490" height="490" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="10"/>\n <g fill="#222">\n <rect x="1" y="248" width="18" height="4"/>\n <rect x="481" y="248" width="18" height="4"/>\n <rect x="248" y="1" width="4" height="18"/>\n <rect x="248" y="481" width="4" height="18"/>\n </g>\n</svg>'; // src/components/MapComponent.ts var MapComponent = class extends AbstractComponent3 { constructor(viewer, plugin) { super(viewer, { className: `psv-map ${CONSTANTS2.CAPTURE_EVENTS_CLASS}` }); this.plugin = plugin; this.state = { visible: false, maximized: false, collapsed: false, galleryWasVisible: false, imgScale: 1, zoom: this.config.defaultZoom, offset: { x: 0, y: 0 }, mouseX: null, mouseY: null, mousedown: false, pinchDist: 0, pinchAngle: 0, hotspotPos: {}, hotspotId: null, hotspotTooltip: null, markers: [], forceRender: false, needsUpdate: false, renderLoop: null, images: {} }; const canvasContainer = document.createElement("div"); canvasContainer.className = "psv-map__container"; canvasContainer.addEventListener("mousedown", this); window.addEventListener("mousemove", this); window.addEventListener("mouseup", this); canvasContainer.addEventListener("touchstart", this); window.addEventListener("touchmove", this); window.addEventListener("touchend", this); canvasContainer.addEventListener("wheel", this); viewer.addEventListener(events.KeypressEvent.type, this); viewer.addEventListener(events.ConfigChangedEvent.type, this); this.canvas = document.createElement("canvas"); this.__setCursor("move"); canvasContainer.appendChild(this.canvas); this.overlay = document.createElement("div"); this.overlay.className = "psv-map__overlay"; canvasContainer.appendChild(this.overlay); this.container.appendChild(canvasContainer); this.container.addEventListener("transitionstart", this); this.container.addEventListener("transitionend", this); if (this.config.buttons.reset) { this.resetButton = new MapResetButton(this); } if (this.config.buttons.maximize) { this.maximizeButton = new MapMaximizeButton(this); } if (this.config.buttons.close) { this.closeButton = new MapCloseButton(this); } if (this.config.buttons.north) { this.compassButton = new MapCompassButton(this); } this.zoomToolbar = new MapZoomToolbar(this); const renderLoop = () => { if (this.isVisible() && (this.state.needsUpdate || this.state.forceRender)) { this.render(); this.state.needsUpdate = false; } this.state.renderLoop = requestAnimationFrame(renderLoop); }; renderLoop(); this.applyConfig(); this.hide(); if (!this.config.visibleOnLoad) { this.toggleCollapse(); } } get config() { return this.plugin.config; } get maximized() { return this.state.maximized; } get collapsed() { return this.state.collapsed; } init() { this.gallery = this.viewer.getPlugin("gallery"); this.gallery?.addEventListener("show-gallery", this); this.gallery?.addEventListener("hide-gallery", this); } destroy() { this.canvas.width = 0; this.canvas.height = 0; window.removeEventListener("touchmove", this); window.removeEventListener("mousemove", this); window.removeEventListener("touchend", this); window.removeEventListener("mouseup", this); this.viewer.removeEventListener(events.KeypressEvent.type, this); this.gallery?.removeEventListener("show-gallery", this); this.gallery?.removeEventListener("hide-gallery", this); cancelAnimationFrame(this.state.renderLoop); super.destroy(); } handleEvent(e) { if (utils2.getMatchingTarget(e, `.${CONSTANTS2.CAPTURE_EVENTS_CLASS}:not(.psv-map)`)) { return; } switch (e.type) { case events.KeypressEvent.type: if (this.state.maximized) { this.__onKeyPress(e); e.preventDefault(); } break; case events.ConfigChangedEvent.type: if (e.containsOptions("lang")) { this.resetButton?.update(); this.closeButton?.update(); this.compassButton?.update(); this.maximizeButton?.update(); } break; case "mousedown": { const event = e; this.state.mouseX = event.clientX; this.state.mouseY = event.clientY; this.state.mousedown = true; e.stopPropagation(); break; } case "touchstart": { const event = e; if (event.touches.length === 1) { this.state.mouseX = event.touches[0].clientX; this.state.mouseY = event.touches[0].clientY; this.state.mousedown = true; } else if (event.touches.length === 2) { ({ distance: this.state.pinchDist, angle: this.state.pinchAngle, center: { x: this.state.mouseX, y: this.state.mouseY } } = utils2.getTouchData(event)); } e.stopPropagation(); e.preventDefault(); break; } case "mousemove": { const event = e; if (this.state.mousedown) { this.__move(event.clientX, event.clientY); e.stopPropagation(); } else if (e.composedPath().includes(this.canvas)) { this.__handleHotspots(event.clientX, event.clientY); } break; } case "touchmove": { const event = e; if (this.state.mousedown && event.touches.length === 1) { this.__move(event.touches[0].clientX, event.touches[0].clientY); e.stopPropagation(); } else if (this.state.mousedown && event.touches.length === 2) { const touchData = utils2.getTouchData(event); const delta = (touchData.distance - this.state.pinchDist) / SYSTEM2.pixelRatio; this.zoom(delta / 100); this.__move(touchData.center.x, touchData.center.y); if (this.state.maximized && !this.config.static) { this.viewer.dynamics.position.step({ yaw: this.state.pinchAngle - touchData.angle }, 0); } ({ distance: this.state.pinchDist, angle: this.state.pinchAngle } = touchData); e.stopPropagation(); } break; } case "mouseup": case "touchend": { const mouse = e.changedTouches?.[0] || e; if (this.state.mousedown) { this.state.mousedown = false; e.stopPropagation(); } if (e.composedPath().includes(this.canvas)) { this.__clickHotspot(mouse.clientX, mouse.clientY); } break; } case "wheel": { const event = e; const delta = event.deltaY / Math.abs(event.deltaY); if (event.ctrlKey) { this.viewer.dynamics.position.step({ yaw: delta / 10 }); } else { this.zoom(-delta / 10); } e.stopPropagation(); e.preventDefault(); break; } case "transitionstart": this.state.forceRender = true; break; case "transitionend": if (!this.state.maximized) { this.overlay.style.display = ""; this.recenter(); } this.state.forceRender = false; this.update(); break; case "hide-gallery": this.__onToggleGallery(false); break; case "show-gallery": if (!e.fullscreen) { this.__onToggleGallery(true); } break; } } applyConfig() { this.container.classList.remove( "psv-map--top-right", "psv-map--top-left", "psv-map--bottom-right", "psv-map--bottom-left", "psv-map--round", "psv-map--square" ); this.container.classList.add(`psv-map--${this.config.position.join("-")}`); this.container.classList.add(`psv-map--${this.config.shape}`); this.container.style.width = this.config.size; this.container.style.height = this.config.size; this.overlay.innerHTML = this.config.overlayImage === null ? "" : getImageHtml(this.config.overlayImage ?? (this.config.shape === "square" ? overlay_square_default : overlay_round_default)); this.resetButton?.applyConfig(); this.closeButton?.applyConfig(); this.compassButton?.applyConfig(); this.maximizeButton?.applyConfig(); if (this.config.static) { this.compassButton?.rotate(0); this.overlay.style.transform = ""; } if (this.config.shape === "square") { this.overlay.style.transform = ""; } this.update(); } isVisible() { return this.state.visible && !this.state.collapsed; } show() { super.show(); this.update(); if (!this.state.maximized) { this.overlay.style.display = ""; } } hide() { super.hide(); this.state.forceRender = false; } /** * Flag for render */ update(clear = true) { this.state.needsUpdate = true; if (clear) { this.state.hotspotPos = {}; this.__resetHotspot(); } } /** * Load a new map image */ reload(url) { delete this.state.images[this.config.imageUrl]; this.config.imageUrl = url; this.state.imgScale = 1; this.__loadImage(this.config.imageUrl, true); this.recenter(); } /** * Clears the offset and zoom level */ reset() { this.state.zoom = this.config.defaultZoom; this.recenter(); } /** * Clears the offset */ recenter() { this.state.offset.x = 0; this.state.offset.y = 0; this.update(); } /** * Switch collapsed mode */ toggleCollapse() { if (this.state.maximized) { this.toggleMaximized(false); } this.state.collapsed = !this.state.collapsed; utils2.toggleClass(this.container, "psv-map--collapsed", this.state.collapsed); if (!this.state.collapsed) { this.reset(); this.plugin.dispatchEvent(new ViewChanged("normal")); } else { this.plugin.dispatchEvent(new ViewChanged("closed")); } this.closeButton?.update(); } /** * Switch maximized mode */ toggleMaximized(dispatchMinimizeEvent = true) { if (this.state.collapsed) { return; } this.state.maximized = !this.state.maximized; utils2.toggleClass(this.container, "psv-map--maximized", this.state.maximized); if (this.state.maximized) { this.state.galleryWasVisible = this.gallery?.isVisible(); this.gallery?.hide(); this.overlay.style.display = "none"; this.plugin.dispatchEvent(new ViewChanged("maximized")); } else { if (this.state.galleryWasVisible) { this.gallery.show(); } if (dispatchMinimizeEvent) { this.plugin.dispatchEvent(new ViewChanged("normal")); } } this.maximizeButton?.update(); } /** * Changes the zoom level */ zoom(d) { this.setZoom(this.state.zoom + d); } /** * Changes the zoom level */ setZoom(value) { this.state.zoom = MathUtils.clamp(value, this.config.minZoom, this.config.maxZoom); this.update(); } /** * Updates the markers */ setMarkers(markers) { this.state.markers = markers; this.update(); } /** * Changes the highlighted hotspot */ setActiveHotspot(hotspotId) { this.state.hotspotId = hotspotId; this.update(false); } render() { if (!this.config.center) { return; } const mapImage = this.__loadImage(this.config.imageUrl); if (!mapImage) { return; } const yaw = this.viewer.getPosition().yaw; const zoom = Math.exp(this.state.zoom) / this.state.imgScale; const center = { x: this.config.center.x * this.state.imgScale, y: this.config.center.y * this.state.imgScale }; const offset = { x: this.state.offset.x * this.state.imgScale, y: this.state.offset.y * this.state.imgScale }; const rotation = this.config.rotation; const yawAndRotation = this.config.static ? 0 : yaw + rotation; if (!this.config.static) { if (this.config.shape === "round") { this.overlay.style.transform = `rotate(${-yawAndRotation}rad)`; } this.compassButton?.rotate(yawAndRotation); } this.zoomToolbar.setText(this.state.zoom); this.canvas.width = this.container.clientWidth * SYSTEM2.pixelRatio; this.canvas.height = this.container.clientHeight * SYSTEM2.pixelRatio; const canvasPos = utils2.getPosition(this.canvas); const canvasW = this.canvas.width; const canvasH = this.canvas.height; const canvasVirtualCenterX = canvasW / 2 / SYSTEM2.pixelRatio; const canvasVirtualCenterY = canvasH / 2 / SYSTEM2.pixelRatio; const context = this.canvas.getContext("2d"); context.clearRect(0, 0, canvasW, canvasH); const mapW = mapImage.width; const mapH = mapImage.height; context.save(); context.translate(canvasW / 2, canvasH / 2); context.rotate(-yawAndRotation); context.scale(zoom, zoom); canvasShadow(context, 0, 0, MAP_SHADOW_BLUR); drawImageHighDpi( context, mapImage, -center.x - offset.x, -center.y - offset.y, mapW, mapH ); context.restore(); [...this.config.hotspots, ...this.state.markers].sort((a, b) => { if (this.state.hotspotId === a.id) { return 1; } if (this.state.hotspotId === b.id) { return -1; } return (a.zIndex ?? 0) - (b.zIndex ?? 0); }).forEach((hotspot) => { const isHover = this.state.hotspotId === hotspot.id; const style = getStyle(this.config.spotStyle, hotspot, isHover); let image; if (style.image) { image = this.__loadImage(style.image); if (!image) { return; } if (!isHover && (hotspot.hoverImage || this.config.spotStyle.hoverImage)) { this.__loadImage(hotspot.hoverImage || this.config.spotStyle.hoverImage, false, false); } } const hotspotPos = { ...offset }; if ("yaw" in hotspot && "distance" in hotspot) { const angle = utils2.parseAngle(hotspot.yaw) + rotation; hotspotPos.x += Math.sin(-angle) * hotspot.distance * this.state.imgScale; hotspotPos.y += Math.cos(-angle) * hotspot.distance * this.state.imgScale; } else if ("x" in hotspot && "y" in hotspot) { hotspotPos.x += center.x - hotspot.x * this.state.imgScale; hotspotPos.y += center.y - hotspot.y * this.state.imgScale; } else { utils2.logWarn(`Hotspot ${hotspot["id"]} is missing position (yaw+distance or x+y)`); return; } const spotPos = projectPoint(hotspotPos, yawAndRotation, zoom); const x = canvasVirtualCenterX - spotPos.x; const y = canvasVirtualCenterY - spotPos.y; this.state.hotspotPos[hotspot.id] = { x: x + canvasPos.x, y: y + canvasPos.y, s: style.size }; context.save(); context.translate(x * SYSTEM2.pixelRatio, y * SYSTEM2.pixelRatio); canvasShadow(context, PIN_SHADOW_OFFSET, PIN_SHADOW_OFFSET, PIN_SHADOW_BLUR); if (image) { drawImageCentered(context, image, style.size); } else { context.fillStyle = style.color; context.beginPath(); context.arc(0, 0, style.size * SYSTEM2.pixelRatio / 2, 0, 2 * Math.PI); context.fill(); if (style.borderColor && style.borderSize) { context.shadowColor = "transparent"; context.strokeStyle = style.borderColor; context.lineWidth = style.borderSize; context.beginPath(); context.arc(0, 0, (style.size + style.borderSize) * SYSTEM2.pixelRatio / 2, 0, 2 * Math.PI); context.stroke(); } } context.restore(); }); const pinImage = this.__loadImage(this.config.pinImage); if (pinImage || this.config.coneColor && this.config.coneSize) { const pinPos = projectPoint(offset, yawAndRotation, zoom); const x = canvasVirtualCenterX - pinPos.x; const y = canvasVirtualCenterY - pinPos.y; const size = this.config.pinSize; const angle = this.config.static ? yaw + rotation : 0; context.save(); context.translate(x * SYSTEM2.pixelRatio, y * SYSTEM2.pixelRatio); context.rotate(angle); if (this.config.coneColor && this.config.coneSize) { const fov = MathUtils.degToRad(this.viewer.state.hFov); const a1 = -Math.PI / 2 - fov / 2; const a2 = a1 + fov; const c = this.config.coneSize * SYSTEM2.pixelRatio; const grad = context.createRadialGradient(0, 0, c / 4, 0, 0, c); grad.addColorStop(0, this.config.coneColor); grad.addColorStop(1, rgbToRgba(this.config.coneColor, 0)); context.beginPath(); context.moveTo(0, 0); context.lineTo(Math.cos(a1) * c, Math.sin(a1) * c); context.arc(0, 0, c, a1, a2, false); context.lineTo(0, 0); context.fillStyle = grad; context.fill(); } if (pinImage) { canvasShadow(context, PIN_SHADOW_OFFSET, PIN_SHADOW_OFFSET, PIN_SHADOW_BLUR); drawImageCentered(context, pinImage, size); } context.restore(); } } /** * Applies mouse movement to the map */ __move(clientX, clientY) { const yaw = this.viewer.getPosition().yaw; const zoom = Math.exp(this.state.zoom); const move = unprojectPoint( { x: this.state.mouseX - clientX, y: this.state.mouseY - clientY }, this.config.static ? 0 : yaw + this.config.rotation, zoom ); this.state.offset.x += move.x; this.state.offset.y += move.y; this.update(); this.state.mouseX = clientX; this.state.mouseY = clientY; } /** * Finds the hotspot under the mouse */ __findHotspot(clientX, clientY) { const k = this.config.spotStyle.size / 2; let hotspotId = null; for (const [id, { x, y }] of Object.entries(this.state.hotspotPos)) { if (clientX > x - k && clientX < x + k && clientY > y - k && clientY < y + k) { hotspotId = id; break; } } return hotspotId; } /** * Updates current hotspot on mouse move and displays tooltip */ __handleHotspots(clientX, clientY) { const hotspotId = this.__findHotspot(clientX, clientY); if (this.state.hotspotId !== hotspotId) { this.__resetHotspot(); if (hotspotId) { let tooltip; if (hotspotId.startsWith(HOTSPOT_MARKER_ID)) { tooltip = this.state.markers.find(({ id }) => id === hotspotId)?.tooltip; } else { tooltip = this.config.hotspots.find(({ id }) => id === hotspotId)?.tooltip; } if (tooltip) { if (typeof tooltip === "string") { tooltip = { content: tooltip }; } const hotspotPos = this.state.hotspotPos[hotspotId]; const viewerPos = utils2.getPosition(this.viewer.container); this.state.hotspotTooltip = this.viewer.createTooltip({ content: tooltip.content, className: tooltip.className, left: hotspotPos.x - viewerPos.x, top: hotspotPos.y - viewerPos.y, box: { width: hotspotPos.s, height: hotspotPos.s } }); } } this.setActiveHotspot(hotspotId); this.__setCursor(hotspotId ? "pointer" : "move"); } } /** * Dispatch event when a hotspot is clicked */ __clickHotspot(clientX, clientY) { const hotspotId = this.__findHotspot(clientX, clientY); if (hotspotId) { this.plugin.dispatchEvent(new SelectHotspot(hotspotId)); if (hotspotId.startsWith(HOTSPOT_MARKER_ID)) { const markerId = hotspotId.substring(HOTSPOT_MARKER_ID.length); this.viewer.getPlugin("markers").gotoMarker(markerId); } if (this.maximized && this.config.minimizeOnHotspotClick) { this.toggleMaximized(); } } this.__resetHotspot(); } __resetHotspot() { this.state.hotspotTooltip?.hide(); this.state.hotspotTooltip = null; this.state.hotspotId = null; } /** * Loads an image and returns the result **synchronously**. * If the image is not already loaded it returns `null` and schedules a new render when the image is ready. */ __loadImage(url, isInit = false, autoRefresh = true) { if (!url) { return null; } if (!this.state.images[url]) { const image = loadImage(url); this.state.images[url] = { loading: true, value: image }; image.onload = () => { if (isInit && Math.max(image.width, image.height) > SYSTEM2.maxCanvasWidth) { this.state.imgScale = SYSTEM2.maxCanvasWidth / Math.max(image.width, image.height); const buffer = document.createElement("canvas"); buffer.width = image.width * this.state.imgScale; buffer.height = image.height * this.state.imgScale; const ctx = buffer.getContext("2d"); ctx.drawImage(image, 0, 0, buffer.width, buffer.height); this.state.images[url].value = buffer; } this.state.images[url].loading = false; if (autoRefresh) { this.update(false); } if (isInit) { this.show(); } }; return null; } if (this.state.images[url].loading) { return null; } return this.state.images[url].value; } __onKeyPress(e) { if (e.matches(CONSTANTS2.KEY_CODES.Escape)) { this.toggleMaximized(); return; } if (!this.viewer.state.keyboardEnabled) { return; } let x = 0; let y = 0; let z = 0; if (e.matches(CONSTANTS2.KEY_CODES.ArrowUp)) y = 1; if (e.matches(CONSTANTS2.KEY_CODES.ArrowDown)) y = -1; if (e.matches(CONSTANTS2.KEY_CODES.ArrowLeft)) x = 1; if (e.matches(CONSTANTS2.KEY_CODES.ArrowRight)) x = -1; if (e.matches(CONSTANTS2.KEY_CODES.Plus)) z = 1; if (e.matches(CONSTANTS2.KEY_CODES.Minus)) z = -1; if (e.matches(CONSTANTS2.KEY_CODES.PageUp)) z = 1; if (e.matches(CONSTANTS2.KEY_CODES.PageDown)) z = -1; if (x || y) { this.state.mouseX = 0; this.state.mouseY = 0; this.__move(x * 10, y * 10); } if (z) { this.zoom(z / 10); } } __setCursor(cursor) { this.canvas.style.cursor = cursor; } __onToggleGallery(visible) { if (!visible) { this.container.style.marginBottom = ""; } else { this.container.style.marginBottom = this.viewer.container.querySelector(".psv-gallery").offsetHeight + 10 + "px"; } } }; // src/icons/pin.svg var pin_default = '<svg viewBox="-20 -20 740 740" xmlns="http://www.w3.org/2000/svg">\n <circle cx="350" cy="350" r="190" fill="white"/>\n <circle cx="350" cy="350" r="150" fill="#1E78E6"/>\n</svg>\n'; // src/MapPlugin.ts var getConfig = utils3.getConfigParser( { imageUrl: null, center: null, rotation: 0, shape: "round", size: "200px", position: ["bottom", "left"], visibleOnLoad: true, overlayImage: void 0, pinImage: pin_default, pinSize: 35, coneColor: "#1E78E6", coneSize: 40, spotStyle: { size: 15, image: null, color: "white", borderSize: 0, borderColor: null, hoverSize: null, hoverImage: null, hoverColor: null, hoverBorderSize: 4, hoverBorderColor: "rgba(255, 255, 255, 0.6)" }, static: false, defaultZoom: 100, minZoom: 20, maxZoom: 200, hotspots: [], minimizeOnHotspotClick: true, buttons: { maximize: true, close: true, reset: true, north: true } }, { spotStyle: (spotStyle, { defValue }) => ({ ...defValue, ...spotStyle }), position: (position, { defValue }) => { return utils3.cleanCssPosition(position, { allowCenter: false, cssOrder: true }) || defValue; }, rotation: (rotation) => utils3.parseAngle(rotation), coneColor: (coneColor) => coneColor ? new Color(coneColor).getStyle() : null, // must be in rgb format defaultZoom: (defaultZoom) => Math.log(defaultZoom / 100), maxZoom: (maxZoom) => Math.log(maxZoom / 100), minZoom: (minZoom) => Math.log(minZoom / 100), buttons: (buttons, { defValue }) => ({ ...defValue, ...buttons }) } ); var _MapPlugin = class _MapPlugin extends AbstractConfigurablePlugin { static withConfig(config) { return [_MapPlugin, config]; } constructor(viewer, config) { super(viewer, config); this.component = new MapComponent(this.viewer, this); } /** * @internal */ init() { super.init(); utils3.checkStylesheet(this.viewer.container, "map-plugin"); this.component.init(); this.markers = this.viewer.getPlugin("markers"); this.viewer.addEventListener(events2.PositionUpdatedEvent.type, this); this.viewer.addEventListener(events2.ZoomUpdatedEvent.type, this); this.viewer.addEventListener(events2.SizeUpdatedEvent.type, this); this.viewer.addEventListener(events2.ReadyEvent.type, this, { once: true }); this.markers?.addEventListener("set-markers", this); this.setHotspots(this.config.hotspots, false); } /** * @internal */ destroy() { this.viewer.removeEventListener(events2.PositionUpdatedEvent.type, this); this.viewer.removeEventListener(events2.ZoomUpdatedEvent.type, this); this.viewer.removeEventListener(events2.SizeUpdatedEvent.type, this); this.viewer.removeEventListener(events2.ReadyEvent.type, this); this.markers?.removeEventListener("set-markers", this); this.component.destroy(); delete this.markers; super.destroy(); } /** * @internal */ handleEvent(e) { switch (e.type) { case events2.ReadyEvent.type: this.component.reload(this.config.imageUrl); break; case events2.PositionUpdatedEvent.type: case events2.ZoomUpdatedEvent.type: this.component.update(); break; case events2.SizeUpdatedEvent.type: if (this.component.maximized) { this.component.update(); } break; case "set-markers": this.component.setMarkers(this.__markersToHotspots(e.markers)); break; default: break; } } setOptions(options) { super.setOptions(options); if (options.center) { this.component.recenter(); } if (options.hotspots !== void 0) { this.setHotspots(options.hotspots); } this.component.applyConfig(); } /** * Hides the map */ hide() { this.component.hide(); } /** * Shows the map */ show() { this.component.show(); } /** * Changes the current zoom level */ setZoom(level) { this.component.setZoom(Math.log(level / 100)); } /** * Closes the map */ close() { if (!this.component.collapsed) { this.component.toggleCollapse(); } } /** * Open the map */ open() { if (this.component.collapsed) { this.component.toggleCollapse(); } } /** * Minimizes the map */ minimize() { if (this.component.maximized) { this.component.toggleMaximized(); } } /** * Maximizes the map */ maximize() { if (!this.component.maximized) { this.component.toggleMaximized(); } } /** * Changes the image of the map * @param rotation Also change the image rotation * @param center Also change the position on the map */ setImage(url, center, rotation) { if (!utils3.isNil(rotation)) { this.config.rotation = utils3.parseAngle(rotation); } if (!utils3.isNil(center)) { this.config.center = center; } this.component.reload(url); } /** * Changes the position on the map */ setCenter(center) { this.config.center = center; this.component.recenter(); } /** * Changes the hotspots on the map */ setHotspots(hotspots, render = true) { const ids = []; let i = 1; hotspots?.forEach((hotspot) => { if (!hotspot.id) { hotspot.id = HOTSPOT_GENERATED_ID + i++; } else if (ids.includes(hotspot.id)) { utils3.logWarn(`Duplicated hotspot id "${hotspot.id}`); } else { ids.push(hotspot.id); } }); this.config.hotspots = hotspots || []; if (render) { this.component.update(); } } /** * Removes all hotspots */ clearHotspots() { this.setHotspots(null); } /** * Changes the highlighted hotspot */ setActiveHotspot(hotspotId) { this.component.setActiveHotspot(hotspotId); } __markersToHotspots(markers) { return markers.filter((marker) => marker.data?.[MARKER_DATA_KEY]).map((marker) => { const hotspot = { ...marker.data[MARKER_DATA_KEY], id: HOTSPOT_MARKER_ID + marker.id, tooltip: marker.config.tooltip }; if ("distance" in hotspot) { hotspot.yaw = marker.state.position.yaw; } else if (!("x" in hotspot) || !("y" in hotspot)) { utils3.logWarn(`Marker #${marker.id} "map" data is missing position (distance or x+y)`); return null; } return hotspot; }).filter((h) => h); } }; _MapPlugin.id = "map"; _MapPlugin.VERSION = "5.14.0"; _MapPlugin.configParser = getConfig; _MapPlugin.readonlyOptions = [ "imageUrl", "visibleOnLoad", "defaultZoom", "buttons" ]; var MapPlugin = _MapPlugin; // src/index.ts DEFAULTS.lang["map"] = "Map"; DEFAULTS.lang["mapMaximize"] = "Maximize"; DEFAULTS.lang["mapMinimize"] = "Minimize"; DEFAULTS.lang["mapNorth"] = "Go to north"; DEFAULTS.lang["mapReset"] = "Reset"; export { MapPlugin, events_exports as events }; //# sourceMappingURL=index.module.js.map