photo-sphere-viewer
Version:
A JavaScript library to display Photo Sphere panoramas
387 lines (334 loc) • 10.4 kB
JavaScript
import { MathUtils } from 'three';
import { AbstractPlugin, CONSTANTS, SYSTEM, utils } from '../..';
import compass from './compass.svg';
import './style.scss';
/**
* @typedef {Object} PSV.plugins.CompassPlugin.Options
* @property {string} [size='120px'] - size of the compass
* @property {string} [position='top left'] - position of the compass
* @property {string} [backgroundSvg] - SVG used as background of the compass
* @property {string} [coneColor='rgba(255, 255, 255, 0.5)'] - color of the cone of the compass
* @property {boolean} [navigation=true] - allows to click on the compass to rotate the viewer
* @property {string} [navigationColor='rgba(255, 0, 0, 0.2)'] - color of the navigation cone
* @property {PSV.plugins.CompassPlugin.Hotspot[]} [hotspots] - small dots visible on the compass (will contain every marker with the "compass" data)
* @property {string} [hotspotColor='rgba(0, 0, 0, 0.5)'] - default color of hotspots
*/
/**
* @typedef {PSV.ExtendedPosition} PSV.plugins.CompassPlugin.Hotspot
* @type {string} [color] - override the global "hotspotColor"
*/
const HOTSPOT_SIZE_RATIO = 1 / 40;
/**
* @summary Adds a compass on the viewer
* @extends PSV.plugins.AbstractPlugin
* @memberof PSV.plugins
*/
export class CompassPlugin extends AbstractPlugin {
static id = 'compass';
/**
* @param {PSV.Viewer} psv
* @param {PSV.plugins.CompassPlugin.Options} options
*/
constructor(psv, options) {
super(psv);
/**
* @member {PSV.plugins.CompassPlugin.Options}
* @private
*/
this.config = {
size : '120px',
backgroundSvg : compass,
coneColor : 'rgba(255, 255, 255, 0.5)',
navigation : true,
navigationColor: 'rgba(255, 0, 0, 0.2)',
hotspotColor : 'rgba(0, 0, 0, 0.5)',
...options,
position : utils.cleanPosition(options.position, { allowCenter: true, cssOrder: true }) || ['top', 'left'],
};
/**
* @private
*/
this.prop = {
visible : true,
mouse : null,
mouseDown: false,
markers : [],
};
/**
* @type {PSV.plugins.MarkersPlugin}
* @private
*/
this.markers = null;
/**
* @member {HTMLElement}
* @readonly
* @private
*/
this.container = document.createElement('div');
this.container.className = `psv-compass psv-compass--${this.config.position.join('-')}`;
this.container.innerHTML = this.config.backgroundSvg;
this.container.style.width = this.config.size;
this.container.style.height = this.config.size;
if (this.config.position[0] === 'center') {
this.container.style.marginTop = `calc(-${this.config.size} / 2)`;
}
if (this.config.position[1] === 'center') {
this.container.style.marginLeft = `calc(-${this.config.size} / 2)`;
}
/**
* @member {HTMLCanvasElement}
* @readonly
* @private
*/
this.canvas = document.createElement('canvas');
this.container.appendChild(this.canvas);
if (this.config.navigation) {
this.container.addEventListener('mouseenter', this);
this.container.addEventListener('mouseleave', this);
this.container.addEventListener('mousemove', this);
this.container.addEventListener('mousedown', this);
this.container.addEventListener('mouseup', this);
this.container.addEventListener('touchstart', this);
this.container.addEventListener('touchmove', this);
this.container.addEventListener('touchend', this);
}
}
/**
* @package
*/
init() {
super.init();
this.markers = this.psv.getPlugin('markers');
this.psv.container.appendChild(this.container);
this.canvas.width = this.container.clientWidth * SYSTEM.pixelRatio;
this.canvas.height = this.container.clientWidth * SYSTEM.pixelRatio;
this.psv.on(CONSTANTS.EVENTS.RENDER, this);
if (this.markers) {
this.markers.on('set-markers', this);
}
}
/**
* @package
*/
destroy() {
this.psv.off(CONSTANTS.EVENTS.RENDER, this);
if (this.markers) {
this.markers.off('set-markers', this);
}
this.psv.container.removeChild(this.container);
delete this.canvas;
delete this.container;
super.destroy();
}
/**
* @private
*/
handleEvent(e) {
switch (e.type) {
case CONSTANTS.EVENTS.RENDER:
this.__update();
break;
case 'set-markers':
this.prop.markers = e.args[0].filter(m => m.data?.compass);
this.__update();
break;
case 'mouseenter':
case 'mousemove':
case 'touchmove':
this.prop.mouse = e.changedTouches?.[0] || e;
if (this.prop.mouseDown) {
this.__click();
}
else {
this.__update();
}
e.stopPropagation();
e.preventDefault();
break;
case 'mousedown':
case 'touchstart':
this.prop.mouseDown = true;
e.stopPropagation();
e.preventDefault();
break;
case 'mouseup':
case 'touchend':
this.prop.mouse = e.changedTouches?.[0] || e;
this.prop.mouseDown = false;
this.__click();
if (e.changedTouches) {
this.prop.mouse = null;
this.__update();
}
e.stopPropagation();
e.preventDefault();
break;
case 'mouseleave':
this.prop.mouse = null;
this.prop.mouseDown = false;
this.__update();
break;
default:
break;
}
}
/**
* @summary Hides the compass
*/
hide() {
this.container.style.display = 'none';
this.prop.visible = false;
}
/**
* @summary Shows the compass
*/
show() {
this.container.style.display = '';
this.prop.visible = true;
}
/**
* @summary Changes the hotspots on the compass
* @param {PSV.plugins.CompassPlugin.Hotspot[]} hotspots
*/
setHotspots(hotspots) {
this.config.hotspots = hotspots;
this.__update();
}
/**
* @summary Removes all hotspots
*/
clearHotspots() {
this.setHotspots(null);
}
/**
* @summary Updates the compass for current zoom and position
* @private
*/
__update() {
const context = this.canvas.getContext('2d');
context.clearRect(0, 0, this.canvas.width, this.canvas.height);
const longitude = this.psv.getPosition().longitude;
const fov = MathUtils.degToRad(this.psv.prop.hFov);
this.__drawCone(context, this.config.coneColor, longitude, fov);
const mouseAngle = this.__getMouseAngle();
if (mouseAngle !== null) {
this.__drawCone(context, this.config.navigationColor, mouseAngle, fov);
}
this.prop.markers.forEach((marker) => {
this.__drawMarker(context, marker);
});
this.config.hotspots?.forEach((spot) => {
if ('longitude' in spot && !('latitude' in spot)) {
spot.latitude = 0;
}
const pos = this.psv.dataHelper.cleanPosition(spot);
this.__drawPoint(context, spot.color || this.config.hotspotColor, pos.longitude, pos.latitude);
});
}
/**
* @summary Rotates the viewer depending on the position of the mouse on the compass
* @private
*/
__click() {
const mouseAngle = this.__getMouseAngle();
if (mouseAngle !== null) {
this.psv.rotate({
longitude: mouseAngle,
latitude : 0,
});
}
}
/**
* @summary Draw a cone
* @param {CanvasRenderingContext2D} context
* @param {string} color
* @param {number} longitude - in viewer reference
* @param {number} fov
* @private
*/
__drawCone(context, color, longitude, fov) {
const a1 = longitude - Math.PI / 2 - fov / 2;
const a2 = a1 + fov;
const c = this.canvas.width / 2;
context.beginPath();
context.moveTo(c, c);
context.lineTo(c + Math.cos(a1) * c, c + Math.sin(a1) * c);
context.arc(c, c, c, a1, a2, false);
context.lineTo(c, c);
context.fillStyle = color;
context.fill();
}
/**
* @summary Draw a Marker
* @param {CanvasRenderingContext2D} context
* @param {PSV.plugins.MarkersPlugin.Marker} marker
* @private
*/
__drawMarker(context, marker) {
let color = this.config.hotspotColor;
if (typeof marker.data.compass === 'string') {
color = marker.data.compass;
}
if (marker.isPoly()) {
context.beginPath();
marker.props.def.forEach(([longitude, latitude], i) => {
const a = longitude - Math.PI / 2;
const d = (latitude + Math.PI / 2) / Math.PI;
const c = this.canvas.width / 2;
context[i === 0 ? 'moveTo' : 'lineTo'](c + Math.cos(a) * c * d, c + Math.sin(a) * c * d);
});
if (marker.isPolygon()) {
context.fillStyle = color;
context.fill();
}
else {
context.strokeStyle = color;
context.lineWidth = Math.max(1, this.canvas.width * HOTSPOT_SIZE_RATIO / 2);
context.stroke();
}
}
else {
const pos = marker.props.position;
this.__drawPoint(context, color, pos.longitude, pos.latitude);
}
}
/**
* @summary Draw a point
* @param {CanvasRenderingContext2D} context
* @param {string} color
* @param {number} longitude - in viewer reference
* @param {number} latitude - in viewer reference
* @private
*/
__drawPoint(context, color, longitude, latitude) {
const a = longitude - Math.PI / 2;
const d = (latitude + Math.PI / 2) / Math.PI;
const c = this.canvas.width / 2;
const r = Math.max(2, this.canvas.width * HOTSPOT_SIZE_RATIO);
context.beginPath();
context.ellipse(
c + Math.cos(a) * c * d, c + Math.sin(a) * c * d,
r, r,
0, 0, Math.PI * 2
);
context.fillStyle = color;
context.fill();
}
/**
* @summary Gets the longitude corresponding to the mouse position on the compass
* @return {number | null}
* @private
*/
__getMouseAngle() {
if (!this.prop.mouse) {
return null;
}
const boundingRect = this.container.getBoundingClientRect();
const mouseX = this.prop.mouse.clientX - boundingRect.left - boundingRect.width / 2;
const mouseY = this.prop.mouse.clientY - boundingRect.top - boundingRect.width / 2;
if (Math.sqrt(mouseX * mouseX + mouseY * mouseY) > boundingRect.width / 2) {
return null;
}
return Math.atan2(mouseY, mouseX) + Math.PI / 2;
}
}