Преглед на файлове

Merge branch 'master' into master-build

pull/952/head
strarsis преди 5 години
родител
ревизия
805f6dab9f
променени са 27 файла, в които са добавени 1089 реда и са изтрити 169 реда
  1. +3
    -0
      .github/CONTRIBUTING.md
  2. +1
    -0
      .github/FUNDING.yml
  3. +3
    -0
      .gitignore
  4. +21
    -0
      .travis.yml
  5. +1
    -1
      COPYING
  6. +1
    -1
      VERSION
  7. +108
    -24
      changelog.md
  8. +16
    -1
      doc/json-config-parameters.md
  9. +1
    -1
      package.json
  10. +109
    -0
      paper/paper.bib
  11. +71
    -0
      paper/paper.md
  12. +114
    -23
      readme.md
  13. +1
    -1
      src/css/pannellum.css
  14. +0
    -22
      src/js/RequestAnimationFrame.js
  15. +10
    -10
      src/js/libpannellum.js
  16. +117
    -77
      src/js/pannellum.js
  17. +0
    -1
      src/standalone/pannellum.htm
  18. Двоични данни
      tests/cube.png
  19. Двоични данни
      tests/equirectangular.png
  20. Двоични данни
      tests/multires.png
  21. +290
    -0
      tests/run_tests.py
  22. +88
    -0
      tests/tests.html
  23. +6
    -3
      utils/build/build.py
  24. +2
    -2
      utils/doc/generate.sh
  25. +11
    -0
      utils/multires/Dockerfile
  26. +10
    -2
      utils/multires/generate.py
  27. +105
    -0
      utils/multires/readme.md

+ 3
- 0
.github/CONTRIBUTING.md Целия файл

@@ -0,0 +1,3 @@
# Contributing

Development takes place at [github.com/mpetroff/pannellum](https://github.com/mpetroff/pannellum). Issues should be opened to report bugs or suggest improvements (or ask questions), and pull requests are welcome. When reporting a bug, please try to include a minimum reproducible example (or at least some sort of example). When proposing changes, please try to match the existing code style, e.g., four space indentation and [JSHint](https://jshint.com/) validation. If your pull request adds an additional configuration parameter, please document it in `doc/json-config-parameters.md`. Pull requests should preferably be created from [feature branches](https://www.atlassian.com/git/tutorials/comparing-workflows/feature-branch-workflow).

+ 1
- 0
.github/FUNDING.yml Целия файл

@@ -0,0 +1 @@
ko_fi: mpetroff

+ 3
- 0
.gitignore Целия файл

@@ -9,3 +9,6 @@ utils/doc/generated_docs

# Ignore IntelliJ Files
.idea

# Ignore logs
tests/*.log

+ 21
- 0
.travis.yml Целия файл

@@ -0,0 +1,21 @@
language: generic
dist: xenial
services:
- xvfb
addons:
chrome: stable
before_install:
- sudo apt-get install -y python3-pillow python3-numpy python3-pip mesa-utils libgl1-mesa-dri libglapi-mesa libosmesa6
- sudo pip3 install selenium
install:
- CHROME_VERSION=`google-chrome --version | sed -r 's/Google Chrome ([0-9]+\.[0-9]+\.[0-9]+).*/\1/'`
- LATEST_CHROMEDRIVER_VERSION=`curl -s "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_${CHROME_VERSION}"`
- curl "https://chromedriver.storage.googleapis.com/${LATEST_CHROMEDRIVER_VERSION}/chromedriver_linux64.zip" -O
- unzip chromedriver_linux64.zip
- sudo mv chromedriver /usr/local/bin
jobs:
include:
- stage: build
script: python3 utils/build/build.py
- stage: test
script: xvfb-run -a python3 tests/run_tests.py

+ 1
- 1
COPYING Целия файл

@@ -1,4 +1,4 @@
Copyright (c) 2011-2018 Matthew Petroff
Copyright (c) 2011-2019 Matthew Petroff

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in


+ 1
- 1
VERSION Целия файл

@@ -1 +1 @@
2.4.1
2.5.3

+ 108
- 24
changelog.md Целия файл

@@ -2,8 +2,92 @@ Changelog
=========


Changes in Pannellum 2.4.1
--------------------------
Changes in Pannellum 2.5.3 (2019-08-21)
---------------------------------------

Bugfixes:
- Fixed loading scenes after previous load failure
- Fixed regression that caused auto-rotation to stop after one revolution
- Fixed behavior of `multiResMinHfov` to match documentation;
default multi-resolution `minHfov` behavior now matches pre-v2.5

Improvements:
- Added optional `scale` parameter to scale hot spots while zooming
- Improved recovery from failed scene loading

API improvements:
- Added optional pitch argument to `startAutoRotate`

Other:
- Added Journal of Open Source Software (JOSS) manuscript
- Numerous documentation improvements per JOSS review (thanks @vsoch and @Fil)
- Improved continuous integration and automated testing support
- Clarified `generate.py` error messages
- Added Dockerfile for `generate.py`


Changes in Pannellum 2.5.2 (2019-07-13)
---------------------------------------

Bugfixes:
- Fixed regression in Video.js plugin


Changes in Pannellum 2.5.1 (2019-07-13)
---------------------------------------

Other:
- Fixed issue with tagging 2.5.0 release


Changes in Pannellum 2.5.0 (2019-07-13)
---------------------------------------

New Features:
- The background color can be set for partial panoramas
(`backgroundColor` parameter)
- Partial panoramas are now supported for the multiresolution format
- An author URL can now be added (`authorURL` parameter)

New API functions:
- Added `fullscreenchange`, `zoomchange`, and `animatefinished` events
- Added `stopMovement` function for stopping all viewer movement

Improvements:
- Equirectangular images are now automatically split into two separate
textures if they're too big (images up to 8192px wide should now be
widely supported)
- Improved render quality for equirectangular images on mobile (using `highp`
for fragment shader)
- Keyboard events for keys not used by the viewer are no longer captured, and
the list of captured keys is configurable (`capturedKeyNumbers` parameter)
- Multiresolution tiles can now be generated from cylindrical panoramas
- Hot spots can now be removed from scenes that aren't currently loaded
- Hot spot cursor is now set via CSS class (so it can be overridden)
- Hot spot link attributes can now be set (`attributes` parameter)
- The "friction" that slows down the viewer motion can now be configured
(`friction` parameter)
- Dynamic scenes are now properly supported for tours

Bugfixes:
- Fixed regression in fallback renderer
- Fixed bug with URL encoding
- Fixed regression in Video.js plugin
- Fixed auto-rotate bug that was manifested when using API to set view
- Fixed full screen bug in Chrome
- Fixed bug with removing event listeners
- Fixed issue with mouse dragging causing jump around yaw limits
- Fixed bug with deleting hot spots
- Fixed bug with fading between scenes

Other:
- Added limited test suite / continuous integration
- Removed `requestAnimationFrame` shim, dropping support for some
older browsers


Changes in Pannellum 2.4.1 (2018-03-03)
---------------------------------------

Bugfixes:

@@ -11,8 +95,8 @@ Bugfixes:
- The API's `loadScene` method now works when no scenes have been loaded yet


Changes in Pannellum 2.4.0
--------------------------
Changes in Pannellum 2.4.0 (2018-01-30)
---------------------------------------

New Features:

@@ -77,16 +161,16 @@ Bugfixes:
- Fixed bug related to removing hot spots


Changes in Pannellum 2.3.2
--------------------------
Changes in Pannellum 2.3.2 (2016-11-20)
---------------------------------------

Bugfixes:

- Fix Chrome fullscreen regression introduced in 2.3.1


Changes in Pannellum 2.3.1
--------------------------
Changes in Pannellum 2.3.1 (2016-11-19)
---------------------------------------

Bugfixes:

@@ -99,8 +183,8 @@ Improvements:
- Better handling of view limits when both limits are in view


Changes in Pannellum 2.3.0
--------------------------
Changes in Pannellum 2.3.0 (2016-10-30)
---------------------------------------

New Features:

@@ -169,8 +253,8 @@ Backwards-Incompatible Configuration Parameter Changes:
can be used with the `config` parameter


Changes in Pannellum 2.2.1
--------------------------
Changes in Pannellum 2.2.1 (2016-03-11)
---------------------------------------

New Features:

@@ -183,8 +267,8 @@ Improvements:
- Better restriction on yaw range


Changes in Pannellum 2.2.0
--------------------------
Changes in Pannellum 2.2.0 (2016-01-27)
---------------------------------------

New Features:

@@ -251,15 +335,15 @@ Other:
- Added JSDoc documentation


Changes in Pannellum 2.1.1
--------------------------
Changes in Pannellum 2.1.1 (2015-01-19)
---------------------------------------

Bugfixes:
- Force subpixel rendering for hot spots


Changes in Pannellum 2.1.0
--------------------------
Changes in Pannellum 2.1.0 (2015-01-14)
---------------------------------------

New Features:

@@ -301,15 +385,15 @@ Other:
removed


Changes in Pannellum 2.0.1
--------------------------
Changes in Pannellum 2.0.1 (2014-08-24)
---------------------------------------

Bugfixes:
- Fix keyboard controls in Safari


Changes in Pannellum 2.0
------------------------
Changes in Pannellum 2.0 (2014-08-22)
-------------------------------------

New Features:

@@ -334,8 +418,8 @@ Bugfixes:
- Numerous


Changes in Pannellum 1.2
------------------------
Changes in Pannellum 1.2 (2012-08-28)
-------------------------------------

New Features:



+ 16
- 1
doc/json-config-parameters.md Целия файл

@@ -23,6 +23,13 @@ If set, the value is displayed as the panorama's author. If no author is
desired, don't set this parameter.


### `authorURL` (string)

If set, the displayed author text is hyperlinked to this URL. If no author URL
is desired, don't set this parameter. The `author` parameter must also be set
for this parameter to have an effect.


### `strings` (dictionary)

Allows user-facing strings to be changed / translated.
@@ -52,7 +59,8 @@ counter-clockwise, and negative is clockwise.

Sets the delay, in milliseconds, to start automatically rotating the panorama
after user activity ceases. This parameter only has an effect if the
`autoRotate` parameter is set.
`autoRotate` parameter is set. Before starting rotation, the viewer is panned
to the initial pitch.


### `autoRotateStopDelay` (number)
@@ -308,6 +316,13 @@ If `clickHandlerFunc` is specified, this function is added as an event handler
for the hot spot's `click` event. The event object and the contents of
`clickHandlerArgs` are passed to the function as arguments.

#### `scale` (boolean)

When `true`, the hot spot is scaled to match changes in the field of view,
relative to the initial field of view. Note that this does not account for
changes in local image scale that occur due to distortions within the viewport.
Defaults to `false`.

### `hotSpotDebug` (boolean)

When `true`, the mouse pointer's pitch and yaw are logged to the console when


+ 1
- 1
package.json Целия файл

@@ -1,7 +1,7 @@
{
"name": "pannellum",
"description": "Pannellum is a lightweight, free, and open source panorama viewer for the web.",
"version": "2.4.1",
"version": "2.5.3",
"bugs": {
"url": "https://github.com/mpetroff/pannellum/issues"
},


+ 109
- 0
paper/paper.bib Целия файл

@@ -0,0 +1,109 @@
@inproceedings{Chen1995,
doi = {10.1145/218380.218395},
year = {1995},
publisher = {{ACM} Press},
author = {Shenchang Eric Chen},
title = {{QuickTime} {VR}},
editor = {Susan G. Mair and Robert Cook},
booktitle = {Proceedings of the 22nd annual conference on computer graphics and interactive techniques ({SIGGRAPH} '95)},
}

@inproceedings{Gede2015,
author = {M\'{a}ty\'{a}s Gede and Zsuzsanna Ungv\'{a}ri and Klaudia Kiss and G\'{a}bor Nagy},
title = {Open-source web-based viewer application for {TLS} surveys in caves},
booktitle = "Proceedings of the 1st {ICA} {European} Symposium on Cartography ({EuroCarto} 2015)",
year = {2015},
editor = {Georg Gartner and Haosheng Huang},
pages = {321--328},
url = {https://cartography.tuwien.ac.at/eurocarto/wp-content/uploads/2015/10/6-7.pdf},
urldate = {2019-07-14},
isbn = {9781907075032},
}

@article{Srinivasan2018,
doi = {10.23925/1980-7651.2018v21;p71-83},
year = {2018},
month = jun,
publisher = {Portal de Revistas {PUC} {SP}},
volume = {21},
pages = {71},
author = {Venkat Srinivasan and T.B. Dinesh and Bhanu Prakash and A. Shalini},
title = {Thirteen ways of looking at institutional history: a model for digital exhibitions from science archives},
journal = {Circumscribere: International Journal for the History of Science},
}

@article{Herault2018,
doi = {10.1186/s40561-018-0074-x},
year = {2018},
month = oct,
publisher = {Springer Nature},
volume = {5},
number = {1},
author = {Romain Christian Herault and Alisa Lincke and Marcelo Milrad and Elin-Sofie Forsg\"{a}rde and Carina Elmqvist},
title = {Using 360-degrees interactive videos in patient trauma treatment education: design, development and evaluation aspects},
journal = {Smart Learning Environments},
}

@incollection{Mohr2018,
doi = {10.1007/978-3-030-04028-4_71},
year = {2018},
publisher = {Springer International Publishing},
pages = {613--620},
author = {Fabian Mohr and Soenke Zehle and Michael Schmitz},
editor = {Rebecca Rouse and Hartmut Koenitz and Mads Haahr},
title = {From Co-Curation to Co-Creation: Users as Collective Authors of Archive-Based Cultural Heritage Narratives},
booktitle = {Interactive Storytelling},
}

@inproceedings{Albrizio2013,
author = {Patrizia Albrizio and Francesco de Virgilio and Ginevra Panzarino and Enrica Zambetta},
title = {WebGIS e divulgazione del dato archeologico con software open source. Il progetto ``Siponto Aperta''},
booktitle = {Proceedings of {ArcheoFOSS}: free, libre and open source software e open format nei processi di ricerca archeologica},
year = {2013},
editor = {Filippo Stanco and Giovanni Gallo},
pages = {101--114},
edition = {8th},
url = {https://www.archaeopress.com/ArchaeopressShop/Public/download.asp?id=%7B14C6CFBD-3371-4DF0-8971-D4ABC24E661E%7D},
urldate = {2019-07-14},
isbn = {9781784912598},
}

@online{ESO2017,
author = {{European Southern Observatory}},
title = {A panorama view of the {VLT}},
year = {2017},
month = sep,
url = {https://www.eso.org/public/images/165309674464e758889a6_eq-ext/},
urldate = {2019-07-14},
}

@techreport{WebGL,
author = {Dean Jackson},
title = {{WebGL} Specification},
month = oct,
url = {https://www.khronos.org/registry/webgl/specs/1.0.3/},
urldate = {2019-07-14},
year = {2014},
type = {Khronos Specification},
institution = {Khronos Group},
}

@techreport{Canvas,
author = {Rik Cabanier and Jatinder Mann and Jay Munro and Tom Wiltzius and Ian Hickson},
title = {{HTML} Canvas {2D} Context},
month = nov,
url = {https://www.w3.org/TR/2015/REC-2dcontext-20151119/},
urldate = {2019-07-14},
year = {2015},
type = {{W3C} Recommendation},
institution = {World Wide Web Consortium ({W3C})},
}

@online{OSM2018,
author = {{OpenStreetMap}},
title = {{Bing} {Streetside} imagery now available in {OpenStreetMap} {iD} editor},
year = {2018},
month = jul,
url = {https://blog.openstreetmap.org/2018/07/03/bing-streetside-in-id/},
urldate = {2019-08-06},
}

+ 71
- 0
paper/paper.md Целия файл

@@ -0,0 +1,71 @@
---
title: 'Pannellum: a lightweight web-based panorama viewer'
tags:
- panoramas
- visualization
- WebGL
authors:
- name: Matthew A. Petroff
orcid: 0000-0002-4436-4215
affiliation: 1
affiliations:
- name: Department of Physics & Astronomy, Johns Hopkins University, Baltimore, Maryland 21218, USA
index: 1
date: 15 July 2019
bibliography: paper.bib
---

# Summary

_Pannellum_ is an interactive web browser-based panorama viewer written in
JavaScript and primarily based on the WebGL web standard [@WebGL] for graphics
processing unit (GPU)-accelerated rendering to the HTML5 ``<canvas>`` element
[@Canvas]. It supports the display of panoramic images that cover the full
sphere, or only parts of it, in equirectangular format, in cube map format, or
in a tiled format that encodes the panorama in multiple resolutions, which
allows for parts of the panorama to be dynamically loaded, reducing data
transfer requirements. In addition to single panoramas, multiple panoramas can
be linked together into a virtual tour, with navigation enabled via
"hot spots," which can also be used to add annotations.

The display of interactive panoramic images on the web dates back to the
mid-1990s, with the development of Apple's QuickTime VR format and associated
web browser plug-ins [@Chen1995]. When development on _Pannellum_ started in
2011, WebGL was a nascent technology, and the majority of existing panorama
viewers for websites were then still based on Java or Adobe Flash plug-ins,
which had supplanted QuickTime as the technology of choice. Since then, both
the viewer and underlying technologies have matured immensely. As an aside, the
name _Pannellum_ was derived as a portmanteau of "panorama" and "vellum," as
this made a unique, pronounceable word, with "vellum" used as a quasi-synonym
to the ``<canvas>`` drawing surface used by the viewer.

An application programming interface (API) is provided, which allows external
code to control the viewer and implement features such as custom buttons or
integration with other webpage elements. One such example is integrating the
viewer with a map [@Gede2015; @Albrizio2013; @OSM2018]; the locations where
panoramas were taken can be displayed as markers on the map, whereby clicking a
marker will open the corresponding panorama in the viewer. Panoramic
videos---videos that cover up to a full 360 degrees of azimuth---are supported
via a bundled extension, which is built using
the API. The viewer's underlying rendering code is separate from its user interface
code, which allows for more extensive customization and tighter integration
with external code, if desired. This rendering code uses a pinhole camera model
for equirectangular panoramas implemented as a WebGL fragment shader, instead
of the more common---and less accurate---approach of mapping the panorama onto
a geometric approximation of a sphere.

_Pannellum_ has proven useful in various fields, when the display of panoramic
images is needed to help digest or present information. These research
applications range from cartography [@Gede2015] to digital humanities
[@Srinivasan2018; @Mohr2018] to archaeology [@Albrizio2013] to medical
education [@Herault2018]. It has also found use in public outreach
applications, such as its use by the European Southern Observatory to display
panoramas of their observatories [@ESO2017]. _Pannellum_ is intended to be used
any time an interactive panorama needs to be displayed on a webpage, be it an
internal research application or a publicly accessible website. It may also
work with certain mobile application frameworks, but such use is not officially
supported. [Documentation](https://pannellum.org/documentation/overview/) and
[interactive examples](https://pannellum.org/documentation/examples/simple-example/)
are provided at [pannellum.org](https://pannellum.org/).

# References

+ 114
- 23
readme.md Целия файл

@@ -1,49 +1,98 @@
# Pannellum

[![Build Status](https://travis-ci.org/mpetroff/pannellum.svg?branch=master)](https://travis-ci.org/mpetroff/pannellum)
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.3334433.svg)](https://doi.org/10.5281/zenodo.3334433)
[![DOI](https://joss.theoj.org/papers/10.21105/joss.01628/status.svg)](https://doi.org/10.21105/joss.01628)

## About

Pannellum is a lightweight, free, and open source panorama viewer for the web. Built using HTML5, CSS3, JavaScript, and WebGL, it is plug-in free. It can be deployed easily as a single file, just 15kB gzipped, and then embedded into pages as an `<iframe>`. A configuration utility is included to generate the required code for embedding.
Pannellum is a lightweight, free, and open source panorama viewer for the web. Built using HTML5, CSS3, JavaScript, and WebGL, it is plug-in free. It can be deployed easily as a single file, just 21kB gzipped, and then embedded into pages as an `<iframe>`. A configuration utility is included to generate the required code for embedding. An API is included for more advanced integrations.

## Getting started

### Hosted examples

A set of [examples](https://pannellum.org/documentation/examples/simple-example/) that demonstrate the viewer's various functionality is hosted on [pannellum.org](https://pannellum.org/). This is the best place to start if you want an overview of Pannellum's functionality. They also provide helpful starting points for creating custom configurations.

### Simple tutorial and configuration utility

If you are just looking to display a single panorama without any advanced functionality, the steps for doing so are covered on the [simple tutorial page](https://pannellum.org/documentation/overview/tutorial/). Said page also includes a utility for easily creating the necessary Pannellum configuration.

### Local testing and self-hosting

If you would like to locally test or self-host Pannellum, continue to the _How to use_ section below.

## How to use
1. Upload `build/pannellum.htm` and a full equirectangular panorama to a web server.
* Due to browser security restrictions, a web server must be used locally as well. With Python 2, one can use `python -m SimpleHTTPServer`, and with Python 3, one can use `python -m http.server`, but any other web server will work as well.
2. Use the included multi-resolution generator (`utils/multires/generate.py`) or configuration tool (`utils/config/configuration.htm`).
3. Insert the generated `<iframe>` code into a page.
1. Upload `build/pannellum.htm` and a full equirectangular panorama to a web server or run a development web server locally.
* Due to browser security restrictions, _a web server must be used locally as well_. With Python 3, one can use `python3 -m http.server`, but any other web server should also work.
2. Use the included multi-resolution generator (`utils/multires/generate.py`), the configuration tool (`utils/config/configuration.htm`), or create a configuration from scratch or based on an [example](https://pannellum.org/documentation/examples/simple-example/).
3. Insert the generated `<iframe>` code into a page, or create a more advanced configuration with [JSON](https://pannellum.org/documentation/reference) or the [API](https://pannellum.org/documentation/api/).

Configuration parameters are documented in the `doc/json-config-parameters.md` file, which is also available at [pannellum.org/documentation/reference/](https://pannellum.org/documentation/reference). API methods are documented inline with [JSDoc](https://jsdoc.app/) comments, and generated documentation is available at [pannellum.org/documentation/api/](https://pannellum.org/documentation/api/).

### Using a minified copy

For final deployment, it is recommended that one use a minified copy of Pannellum instead of using the source files in `src` directly. The easiest method is to download the most recent [release](https://github.com/mpetroff/pannellum/releases) and use the pre-built copy of either `pannellum.htm` or `pannellum.js` & `pannellum.css`. If you wish to make changes to Pannellum or use the latest development copy of the code, follow the instructions in the _Building_ section below to create `build/pannellum.htm`, `build/pannellum.js`, and `build/pannellum.css`.

### Using `generate.py` to create multires panoramas
To be able to create multiresolution panoramas, you need to have the `nona` program installed, which is available as part of [Hugin](http://hugin.sourceforge.net/), as well as Python with the [Pillow](https://pillow.readthedocs.org/) package. Then, run

```
python generate.py pano_image.jpg
python3 generate.py pano_image.jpg
```

in the `utils/multires` directory. This will generate all the image tiles and the `config.json` file in the `./output` folder by default. For this to work, `nona` needs to be on the system path; otherwise, the location of `nona` can be specified using the `-n` flag. On a Unix-like platform, with `nona` already on the system path use:

```bash
$ cd utils/multires
$ python3 generate.py pano_image.jpg
```

where `pano_image.jpg` is the filename of your equirectangular panorama. If `nona` is not on the system path, use:

```bash
$ cd utils/multires
$ python3 generate.py -n /path/to/nona pano_image.jpg
```

For a complete list of options, run:

```bash
$ python3 generate.py --help
```

To view the generated configuration, run:

```bash
$ cd ../..
$ python3 -m http.server
```

in the `utils/multires` directory. This will generate all the image tiles and the `config.json` file in the `./output` folder by default. For this to work, `nona` needs to be on the system path; otherwise, the location of `nona` can be specified using the `-n` flag, e.g. `python generate.py -n /path/to/nona pano_image.jpg`.
This goes back to the root directory of the repository and starts a local development web server. Then open http://localhost:8000/src/standalone/pannellum.htm?config=../../utils/multires/output/config.json in your web browser of choice.

## Examples

Examples using both the minified version and the version in the `src` directory are included in the `examples` directory.
## Bundled examples

Examples using both the minified version and the version in the `src` directory are included in the `examples` directory. These can be viewed by starting a local web server in the root of the repository, e.g., by running:
```bash
$ python3 -m http.server
```
in the directory containing this readme file, and then navigating to the hosted HTML files using a web browser; note that the examples use files from the `src` directory, so **the web server must be started from the repository root, not the `examples` directory**. For the `example-minified.htm` example to work, a minified copy of Pannellum must first be built; see the _Building_ section below for details.

Additional examples are available at [pannellum.org](https://pannellum.org/documentation/examples/simple-example/).

## Browser Compatibility

Since Pannellum is built with recent web standards, it requires a modern browser to function.
Since Pannellum is built with web standards, it requires a modern browser to function.

#### Full support (with appropriate graphics drivers):
* Firefox 10+
* Chrome 15+
* Firefox 23+
* Chrome 24+
* Safari 8+
* Internet Explorer 11+
* Edge

#### Almost full support (no full screen):
* Firefox 4+
* Chrome 9+

#### Partial support (WebGL support must first be enabled in preferences)

* Safari 5.1+

#### No support:
Internet Explorer 10 and previous
The support list is based on feature support. As only recent browsers are tested, there may be regressions in older browsers.

#### Not officially supported:

@@ -54,7 +103,45 @@ Mobile / app frameworks are not officially supported. They may work, but they're
All user-facing strings can be changed using the `strings` configuration parameter. There exists a [third-party respository of user-contributed translations](https://github.com/DanielBiegler/pannellum-translation) that can be used with this configuration option.

## Building
The `utils` folder contains the required build tools, with the exception of Python 3.2+ and Java installations. To build a minified version of Pannellum, run either `build.sh` or `build.bat` depending on your platform.
The `utils` folder contains the required build tools, with the exception of Python 3.2+ and Java installations. To build a minified version of Pannellum, run either `build.sh` or `build.bat` depending on your platform. On a Unix-like platform:

```bash
$ cd utils/build
$ ./build.sh
```

If successful, this should create `build/pannellum.htm`, `build/pannellum.js`, and `build/pannellum.css`, relative to the root directory of the repository.

## Tests

A minimal [Selenium](https://www.seleniumhq.org/)-based test suite is located in the `tests` directory. The tests can be executed by running:

```bash
python3 run_tests.py
```

A Selenium-driven web browser (with a Chrome driver, by default) is created, and screenshots are generated
and compared against previously generated ones in [tests](tests). For example, to regenerate the screenshots
one can run:

```bash
$ python3 tests/run_tests.py --create-ref
```

And to simply run the tests to compare to, eliminate that argument. By default, a random
port is selected, along with other arguments. One can see usage via:

```bash
$ python tests/run_tests.py --help
```

Continuous integration tests are run via [Travis CI](https://travis-ci.org/mpetroff/pannellum). Running the tests locally requires Python 3, the Selenium Python bindings, [Pillow](https://pillow.readthedocs.io/), [NumPy](https://www.numpy.org/), and either Firefox & [geckodriver](https://github.com/mozilla/geckodriver) or Chrome & [ChromeDriver](https://chromedriver.chromium.org/).

## Seeking support
If you wish to ask a question or report a bug, please open an issue at [github.com/mpetroff/pannellum](https://github.com/mpetroff/pannellum). See the _Contributing_ section below for more details.

## Contributing
Development takes place at [github.com/mpetroff/pannellum](https://github.com/mpetroff/pannellum). Issues should be opened to report bugs or suggest improvements (or ask questions), and pull requests are welcome. When reporting a bug, please try to include a minimum reproducible example (or at least some sort of example). When proposing changes, please try to match the existing code style, e.g., four space indentation and [JSHint](https://jshint.com/) validation. If your pull request adds an additional configuration parameter, please document it in `doc/json-config-parameters.md`. Pull requests should preferably be created from [feature branches](https://www.atlassian.com/git/tutorials/comparing-workflows/feature-branch-workflow).

## License
Pannellum is distributed under the MIT License. For more information, read the file `COPYING` or peruse the license [online](https://github.com/mpetroff/pannellum/blob/master/COPYING).
@@ -67,3 +154,7 @@ The panoramic image provided with the examples is licensed under the [Creative C

* [Matthew Petroff](http://mpetroff.net/), Original Author
* [three.js](https://github.com/mrdoob/three.js) r40, Former Underlying Framework

If used as part of academic research, please cite:

> Petroff, Matthew A. "Pannellum: a lightweight web-based panorama viewer." _Journal of Open Source Software_ 4, no. 40 (2019): 1628. [doi:10.21105/joss.01628](https://doi.org/10.21105/joss.01628)

+ 1
- 1
src/css/pannellum.css Целия файл

@@ -287,7 +287,7 @@
table-layout: fixed;
}

.pnlm-info-box a {
.pnlm-info-box a, .pnlm-author-box a {
color: #fff;
word-wrap: break-word;
overflow-wrap: break-word;


+ 0
- 22
src/js/RequestAnimationFrame.js Целия файл

@@ -1,22 +0,0 @@
/**
* Provides requestAnimationFrame in a cross browser way.
* http://paulirish.com/2011/requestanimationframe-for-smart-animating/
*/

if ( !window.requestAnimationFrame ) {

window.requestAnimationFrame = ( function() {

return window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function( /* function FrameRequestCallback */ callback, /* DOMElement Element */ element ) {

window.setTimeout( callback, 1000 / 60 );

};

} )();

}

+ 10
- 10
src/js/libpannellum.js Целия файл

@@ -1,6 +1,6 @@
/*
* libpannellum - A WebGL and CSS 3D transform based Panorama Renderer
* Copyright (c) 2012-2018 Matthew Petroff
* Copyright (c) 2012-2019 Matthew Petroff
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -266,9 +266,9 @@ function Renderer(container) {
faceImg.onload = onLoad;
faceImg.onerror = incLoaded; // ignore missing face to support partial fallback image
if (imageType == 'multires') {
faceImg.src = encodeURI(path.replace('%s', sides[s]) + '.' + image.extension);
faceImg.src = path.replace('%s', sides[s]) + '.' + image.extension;
} else {
faceImg.src = encodeURI(image[s].src);
faceImg.src = image[s].src;
}
}
fillMissingFaces(fallbackImgSize);
@@ -304,9 +304,9 @@ function Renderer(container) {
}
} else if (imageType == 'cubemap') {
if (cubeImgWidth > gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE)) {
console.log('Error: The image is too big; it\'s ' + width + 'px wide, '+
console.log('Error: The image is too big; it\'s ' + cubeImgWidth + 'px wide, ' +
'but this device\'s maximum supported size is ' + maxWidth + 'px.');
throw {type: 'webgl size error', width: width, maxWidth: maxWidth};
throw {type: 'webgl size error', width: cubeImgWidth, maxWidth: maxWidth};
}
}

@@ -695,9 +695,9 @@ function Renderer(container) {
program.nodeCache.length > program.currentNodes.length + 50) {
// Remove older nodes from cache
var removed = program.nodeCache.splice(200, program.nodeCache.length - 200);
for (var i = 0; i < removed.length; i++) {
for (var j = 0; j < removed.length; j++) {
// Explicitly delete textures
gl.deleteTexture(removed[i].texture);
gl.deleteTexture(removed[j].texture);
}
}
program.currentNodes = [];
@@ -1153,7 +1153,7 @@ function Renderer(container) {
});
this.image.addEventListener('load', loadFn);
this.image.addEventListener('error', loadFn); // ignore missing tile file to support partial image, otherwise retry loop causes high CPU load
};
}

TextureImageLoader.prototype.loadTexture = function(src, texture, callback) {
this.texture = texture;
@@ -1166,7 +1166,7 @@ function Renderer(container) {
this.src = src;
this.texture = texture;
this.callback = callback;
};
}

function releaseTextureImageLoader(til) {
if (pendingTextureRequests.length) {
@@ -1196,7 +1196,7 @@ function Renderer(container) {
* @param {MultiresNode} node - Input node.
*/
function processNextTile(node) {
loadTexture(node, encodeURI(node.path + '.' + image.extension), function(texture, loaded) {
loadTexture(node, node.path + '.' + image.extension, function(texture, loaded) {
node.texture = texture;
node.textureLoaded = loaded ? 2 : 1;
}, globalParams.crossOrigin);


+ 117
- 77
src/js/pannellum.js Целия файл

@@ -1,6 +1,6 @@
/*
* Pannellum - An HTML5 based Panorama Viewer
* Copyright (c) 2011-2018 Matthew Petroff
* Copyright (c) 2011-2019 Matthew Petroff
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -68,7 +68,8 @@ var config,
specifiedPhotoSphereExcludes = [],
update = false, // Should we update when still to render dynamic content
eps = 1e-6,
hotspotsCreated = false;
hotspotsCreated = false,
destroyed = false;

var defaultConfig = {
hfov: 100,
@@ -134,7 +135,7 @@ defaultConfig.strings = {
'%spx wide. Try another device.' +
' (If you\'re the author, try scaling down the image.)', // Two substitutions: image width, max image width
unknownError: 'Unknown error. Check developer console.',
}
};

// Initialize container
container = typeof container === 'string' ? document.getElementById(container) : container;
@@ -446,6 +447,13 @@ function init() {
if (config.draggable)
uiContainer.classList.add('pnlm-grab');
uiContainer.classList.remove('pnlm-grabbing');

// Properly handle switching to dynamic scenes
update = config.dynamicUpdate === true;
if (config.dynamic && update) {
panoImage = config.panorama;
onImageLoad();
}
}

/**
@@ -457,7 +465,7 @@ function init() {
function absoluteURL(url) {
// From http://stackoverflow.com/a/19709846
return new RegExp('^(?:[a-z]+:)?//', 'i').test(url) || url[0] == '/' || url.slice(0, 5) == 'blob:';
};
}

/**
* Create renderer and initialize event listeners once image is loaded.
@@ -617,6 +625,7 @@ function anError(errorMsg) {
infoDisplay.load.box.style.display = 'none';
infoDisplay.errorMsg.style.display = 'table';
error = true;
loaded = undefined;
renderContainer.style.display = 'none';
fireEvent('error', errorMsg);
}
@@ -630,6 +639,7 @@ function clearError() {
infoDisplay.load.box.style.display = 'none';
infoDisplay.errorMsg.style.display = 'none';
error = false;
renderContainer.style.display = 'block';
fireEvent('errorcleared');
}
}
@@ -1211,12 +1221,11 @@ function keyRepeat() {
latestInteraction = Date.now();

// If auto-rotate
var inactivityInterval = Date.now() - latestInteraction;
if (config.autoRotate) {
// Pan
if (newTime - prevTime > 0.001) {
var timeDiff = (newTime - prevTime) / 1000;
var yawDiff = (speed.yaw / timeDiff * diff - config.autoRotate * 0.2) * timeDiff
var yawDiff = (speed.yaw / timeDiff * diff - config.autoRotate * 0.2) * timeDiff;
yawDiff = (-config.autoRotate > 0 ? 1 : -1) * Math.min(Math.abs(config.autoRotate * timeDiff), Math.abs(yawDiff));
config.yaw += yawDiff;
}
@@ -1349,6 +1358,10 @@ function animateInit() {
* @private
*/
function animate() {
if (destroyed) {
return;
}

render();
if (autoRotateStart)
clearTimeout(autoRotateStart);
@@ -1399,6 +1412,17 @@ function render() {
var tmpyaw;

if (loaded) {
var canvas = renderer.getCanvas();

if (config.autoRotate !== false) {
// When auto-rotating this check needs to happen first (see issue #764)
if (config.yaw > 180) {
config.yaw -= 360;
} else if (config.yaw < -180) {
config.yaw += 360;
}
}

// Keep a tmp value of yaw for autoRotate comparison later
tmpyaw = config.yaw;

@@ -1406,8 +1430,7 @@ function render() {
var hoffcut = 0,
voffcut = 0;
if (config.avoidShowingBackground) {
var canvas = renderer.getCanvas(),
hfov2 = config.hfov / 2,
var hfov2 = config.hfov / 2,
vfov2 = Math.atan2(Math.tan(hfov2 / 180 * Math.PI), (canvas.width / canvas.height)) * 180 / Math.PI,
transposed = config.vaov > config.haov;
if (transposed) {
@@ -1433,10 +1456,14 @@ function render() {
config.yaw = Math.max(minYaw, Math.min(maxYaw, config.yaw));
}
if (config.yaw > 180) {
config.yaw -= 360;
} else if (config.yaw < -180) {
config.yaw += 360;
if (!(config.autoRotate !== false)) {
// When not auto-rotating, this check needs to happen after the
// previous check (see issue #698)
if (config.yaw > 180) {
config.yaw -= 360;
} else if (config.yaw < -180) {
config.yaw += 360;
}
}

// Check if we autoRotate in a limited by min and max yaw
@@ -1447,7 +1474,6 @@ function render() {
}

// Ensure the calculated pitch is within min and max allowed
var canvas = renderer.getCanvas();
var vfov = 2 * Math.atan(Math.tan(config.hfov / 180 * Math.PI * 0.5) /
(canvas.width / canvas.height)) / Math.PI * 180;
var minPitch = config.minPitch + vfov / 2,
@@ -1502,7 +1528,7 @@ Quaternion.prototype.multiply = function(q) {
this.x*q.w + this.w*q.x + this.y*q.z - this.z*q.y,
this.y*q.w + this.w*q.y + this.z*q.x - this.x*q.z,
this.z*q.w + this.w*q.z + this.x*q.y - this.y*q.x);
}
};

/**
* Converts quaternion to Euler angles.
@@ -1516,7 +1542,7 @@ Quaternion.prototype.toEulerAngles = function() {
psi = Math.atan2(2 * (this.w * this.z + this.x * this.y),
1 - 2 * (this.y * this.y + this.z * this.z));
return [phi, theta, psi];
}
};

/**
* Converts device orientation API Tait-Bryan angles to a quaternion.
@@ -1564,6 +1590,8 @@ function computeQuaternion(alpha, beta, gamma) {
* @param {DeviceOrientationEvent} event - Device orientation event.
*/
function orientationListener(e) {
if (e.hasOwnProperty('requestPermission'))
e.requestPermission()
var q = computeQuaternion(e.alpha, e.beta, e.gamma).toEulerAngles();
if (typeof(orientation) == 'number' && orientation < 10) {
// This kludge is necessary because iOS sometimes provides a few stale
@@ -1652,10 +1680,10 @@ function renderInitCallback() {
preview = undefined;
}
loaded = true;

fireEvent('load');
animateInit();

fireEvent('load');
}

/**
@@ -1669,7 +1697,7 @@ function createHotSpot(hs) {
hs.yaw = Number(hs.yaw) || 0;

var div = document.createElement('div');
div.className = 'pnlm-hotspot-base'
div.className = 'pnlm-hotspot-base';
if (hs.cssClass)
div.className += ' ' + hs.cssClass;
else
@@ -1682,24 +1710,24 @@ function createHotSpot(hs) {
var a;
if (hs.video) {
var video = document.createElement('video'),
p = hs.video;
if (config.basePath && !absoluteURL(p))
p = config.basePath + p;
video.src = sanitizeURL(p);
vidp = hs.video;
if (config.basePath && !absoluteURL(vidp))
vidp = config.basePath + vidp;
video.src = sanitizeURL(vidp);
video.controls = true;
video.style.width = hs.width + 'px';
renderContainer.appendChild(div);
span.appendChild(video);
} else if (hs.image) {
var p = hs.image;
if (config.basePath && !absoluteURL(p))
p = config.basePath + p;
var imgp = hs.image;
if (config.basePath && !absoluteURL(imgp))
imgp = config.basePath + imgp;
a = document.createElement('a');
a.href = sanitizeURL(hs.URL ? hs.URL : p);
a.href = sanitizeURL(hs.URL ? hs.URL : imgp);
a.target = '_blank';
span.appendChild(a);
var image = document.createElement('img');
image.src = sanitizeURL(p);
image.src = sanitizeURL(imgp);
image.style.width = hs.width + 'px';
image.style.paddingTop = '5px';
renderContainer.appendChild(div);
@@ -1751,7 +1779,7 @@ function createHotSpot(hs) {
span.className += ' pnlm-pointer';
}
hs.div = div;
};
}

/**
* Creates hot spot elements for the current scene.
@@ -1831,6 +1859,9 @@ function renderHotSpot(hs) {
coord[1] += (canvasHeight - hs.div.offsetHeight) / 2;
var transform = 'translate(' + coord[0] + 'px, ' + coord[1] +
'px) translateZ(9999px) rotate(' + config.roll + 'deg)';
if (hs.scale) {
transform += ' scale(' + (origHfov/config.hfov) / z + ')';
}
hs.div.style.webkitTransform = transform;
hs.div.style.MozTransform = transform;
hs.div.style.transform = transform;
@@ -1975,7 +2006,15 @@ function processOptions(isPreview) {
break;
case 'author':
infoDisplay.author.innerHTML = config.strings.bylineLabel.replace('%s', escapeHTML(config[key]));
var authorText = escapeHTML(config[key]);
if (config.authorURL) {
var authorLink = document.createElement('a');
authorLink.href = sanitizeURL(config['authorURL']);
authorLink.target = '_blank';
authorLink.innerHTML = escapeHTML(config[key]);
authorText = authorLink.outerHTML;
}
infoDisplay.author.innerHTML = config.strings.bylineLabel.replace('%s', authorText);
infoDisplay.container.style.display = 'inline';
break;
@@ -1985,7 +2024,7 @@ function processOptions(isPreview) {
link.target = '_blank';
link.textContent = 'Click here to view this panorama in an alternative viewer.';
var message = document.createElement('p');
message.textContent = 'Your browser does not support WebGL.'
message.textContent = 'Your browser does not support WebGL.';
message.appendChild(document.createElement('br'));
message.appendChild(link);
infoDisplay.errorMsg.innerHTML = ''; // Removes all children nodes
@@ -2154,12 +2193,12 @@ function zoomOut() {
function constrainHfov(hfov) {
// Keep field of view within bounds
var minHfov = config.minHfov;
if (config.type == 'multires' && renderer && config.multiResMinHfov) {
if (config.type == 'multires' && renderer && !config.multiResMinHfov) {
minHfov = Math.min(minHfov, renderer.getCanvas().width / (config.multiRes.cubeResolution / 90 * 0.9));
}
if (minHfov > config.maxHfov) {
// Don't change view if bounds don't make sense
console.log('HFOV bounds do not make sense (minHfov > maxHfov).')
console.log('HFOV bounds do not make sense (minHfov > maxHfov).');
return config.hfov;
}
var newHfov = config.hfov;
@@ -2175,8 +2214,7 @@ function constrainHfov(hfov) {
var canvas = renderer.getCanvas();
newHfov = Math.min(newHfov,
Math.atan(Math.tan((config.maxPitch - config.minPitch) / 360 * Math.PI) /
canvas.height * canvas.width)
* 360 / Math.PI);
canvas.height * canvas.width) * 360 / Math.PI);
}
return newHfov;
}
@@ -2227,6 +2265,8 @@ function load() {
* @param {boolean} [fadeDone] - If `true`, fade setup is skipped.
*/
function loadScene(sceneId, targetPitch, targetYaw, targetHfov, fadeDone) {
if (!loaded)
fadeDone = true; // Don't try to fade when there isn't a scene loaded
loaded = false;
animatedMove = {};
@@ -2291,13 +2331,6 @@ function loadScene(sceneId, targetPitch, targetYaw, targetHfov, fadeDone) {
}
fireEvent('scenechange', sceneId);
load();

// Properly handle switching to dynamic scenes
update = config.dynamicUpdate === true;
if (config.dynamic) {
panoImage = config.panorama;
onImageLoad();
}
}

/**
@@ -2410,9 +2443,9 @@ this.setPitch = function(pitch, animated, callback, callbackArgs) {
'startPosition': config.pitch,
'endPosition': pitch,
'duration': animated
}
};
if (typeof callback == 'function')
setTimeout(function(){callback(callbackArgs)}, animated);
setTimeout(function(){callback(callbackArgs);}, animated);
} else {
config.pitch = pitch;
}
@@ -2471,22 +2504,22 @@ this.setYaw = function(yaw, animated, callback, callbackArgs) {
return this;
}
animated = animated == undefined ? 1000: Number(animated);
yaw = ((yaw + 180) % 360) - 180 // Keep in bounds
yaw = ((yaw + 180) % 360) - 180; // Keep in bounds
if (animated) {
// Animate in shortest direction
if (config.yaw - yaw > 180)
yaw += 360
yaw += 360;
else if (yaw - config.yaw > 180)
yaw -= 360
yaw -= 360;

animatedMove.yaw = {
'startTime': Date.now(),
'startPosition': config.yaw,
'endPosition': yaw,
'duration': animated
}
};
if (typeof callback == 'function')
setTimeout(function(){callback(callbackArgs)}, animated);
setTimeout(function(){callback(callbackArgs);}, animated);
} else {
config.yaw = yaw;
}
@@ -2551,9 +2584,9 @@ this.setHfov = function(hfov, animated, callback, callbackArgs) {
'startPosition': config.hfov,
'endPosition': constrainHfov(hfov),
'duration': animated
}
};
if (typeof callback == 'function')
setTimeout(function(){callback(callbackArgs)}, animated);
setTimeout(function(){callback(callbackArgs);}, animated);
} else {
setHfov(hfov);
}
@@ -2614,7 +2647,7 @@ this.lookAt = function(pitch, yaw, hfov, animated, callback, callbackArgs) {
if (typeof callback == 'function')
callback(callbackArgs);
return this;
}
};

/**
* Returns the panorama's north offset.
@@ -2689,15 +2722,19 @@ this.setHorizonPitch = function(pitch) {

/**
* Start auto rotation.
*
* Before starting rotation, the viewer is panned to `pitch`.
* @memberof Viewer
* @instance
* @param {number} [speed] - Auto rotation speed / direction. If not specified, previous value is used.
* @param {number} [pitch] - The pitch to rotate at. If not specified, inital pitch is used.
* @returns {Viewer} `this`
*/
this.startAutoRotate = function(speed) {
this.startAutoRotate = function(speed, pitch) {
speed = speed || autoRotateSpeed || 1;
pitch = pitch === undefined ? origPitch : pitch;
config.autoRotate = speed;
_this.lookAt(origPitch, undefined, origHfov, 3000);
_this.lookAt(pitch, undefined, origHfov, 3000);
animateInit();
return this;
};
@@ -2723,7 +2760,7 @@ this.stopAutoRotate = function() {
this.stopMovement = function() {
stopAnimation();
speed = {'yaw': 0, 'pitch': 0, 'hfov': 0};
}
};

/**
* Returns the panorama renderer.
@@ -2749,7 +2786,7 @@ this.setUpdate = function(bool) {
else
animateInit();
return this;
}
};

/**
* Calculate panorama pitch and yaw from location of mouse event.
@@ -2760,7 +2797,7 @@ this.setUpdate = function(bool) {
*/
this.mouseEventToCoords = function(event) {
return mouseEventToCoords(event);
}
};

/**
* Change scene being viewed.
@@ -2776,7 +2813,7 @@ this.loadScene = function(sceneId, pitch, yaw, hfov) {
if (loaded !== false)
loadScene(sceneId, pitch, yaw, hfov);
return this;
}
};

/**
* Get ID of current scene.
@@ -2786,7 +2823,7 @@ this.loadScene = function(sceneId, pitch, yaw, hfov) {
*/
this.getScene = function() {
return config.scene;
}
};

/**
* Add a new scene.
@@ -2824,7 +2861,7 @@ this.removeScene = function(sceneId) {
this.toggleFullscreen = function() {
toggleFullscreen();
return this;
}
};

/**
* Get configuration of current scene.
@@ -2834,7 +2871,7 @@ this.toggleFullscreen = function() {
*/
this.getConfig = function() {
return config;
}
};

/**
* Get viewer's container element.
@@ -2844,7 +2881,7 @@ this.getConfig = function() {
*/
this.getContainer = function() {
return container;
}
};

/**
* Add a new hot spot.
@@ -2870,7 +2907,7 @@ this.addHotSpot = function(hs, sceneId) {
}
initialConfig.scenes[id].hotSpots.push(hs); // Add hot spot to config
} else {
throw 'Invalid scene ID!'
throw 'Invalid scene ID!';
}
}
if (sceneId === undefined || config.scene == sceneId) {
@@ -2880,7 +2917,7 @@ this.addHotSpot = function(hs, sceneId) {
renderHotSpot(hs);
}
return this;
}
};

/**
* Remove a hot spot.
@@ -2912,11 +2949,11 @@ this.removeHotSpot = function(hotSpotId, sceneId) {
if (initialConfig.scenes.hasOwnProperty(sceneId)) {
if (!initialConfig.scenes[sceneId].hasOwnProperty('hotSpots'))
return false;
for (var i = 0; i < initialConfig.scenes[sceneId].hotSpots.length; i++) {
if (initialConfig.scenes[sceneId].hotSpots[i].hasOwnProperty('id') &&
initialConfig.scenes[sceneId].hotSpots[i].id === hotSpotId) {
for (var j = 0; j < initialConfig.scenes[sceneId].hotSpots.length; j++) {
if (initialConfig.scenes[sceneId].hotSpots[j].hasOwnProperty('id') &&
initialConfig.scenes[sceneId].hotSpots[j].id === hotSpotId) {
// Remove hot spot from configuration
initialConfig.scenes[sceneId].hotSpots.splice(i, 1);
initialConfig.scenes[sceneId].hotSpots.splice(j, 1);
return true;
}
}
@@ -2924,7 +2961,7 @@ this.removeHotSpot = function(hotSpotId, sceneId) {
return false;
}
}
}
};

/**
* This method should be called if the viewer's container is resized.
@@ -2934,7 +2971,7 @@ this.removeHotSpot = function(hotSpotId, sceneId) {
this.resize = function() {
if (renderer)
onDocumentResize();
}
};

/**
* Check if a panorama is loaded.
@@ -2944,7 +2981,7 @@ this.resize = function() {
*/
this.isLoaded = function() {
return loaded;
}
};

/**
* Check if device orientation control is supported.
@@ -2954,7 +2991,7 @@ this.isLoaded = function() {
*/
this.isOrientationSupported = function() {
return orientationSupport || false;
}
};

/**
* Stop using device orientation.
@@ -2963,7 +3000,7 @@ this.isOrientationSupported = function() {
*/
this.stopOrientation = function() {
stopOrientation();
}
};

/**
* Start using device orientation (does nothing if not supported).
@@ -2973,7 +3010,7 @@ this.stopOrientation = function() {
this.startOrientation = function() {
if (orientationSupport)
startOrientation();
}
};

/**
* Check if device orientation control is currently activated.
@@ -2983,7 +3020,7 @@ this.startOrientation = function() {
*/
this.isOrientationActive = function() {
return Boolean(orientation);
}
};

/**
* Subscribe listener to specified event.
@@ -2997,7 +3034,7 @@ this.on = function(type, listener) {
externalEventListeners[type] = externalEventListeners[type] || [];
externalEventListeners[type].push(listener);
return this;
}
};

/**
* Remove an event listener (or listeners).
@@ -3027,7 +3064,7 @@ this.off = function(type, listener) {
delete externalEventListeners[type];
}
return this;
}
};

/**
* Fire listeners attached to specified event.
@@ -3049,6 +3086,9 @@ function fireEvent(type) {
* @memberof Viewer
*/
this.destroy = function() {
destroyed = true;
clearTimeout(autoRotateStart);

if (renderer)
renderer.destroy();
if (listenersAdded) {
@@ -3067,7 +3107,7 @@ this.destroy = function() {
}
container.innerHTML = '';
container.classList.remove('pnlm-container');
}
};

}



+ 0
- 1
src/standalone/pannellum.htm Целия файл

@@ -16,7 +16,6 @@
</noscript>
</div>
<script type="text/javascript" src="../js/libpannellum.js"></script>
<script type="text/javascript" src="../js/RequestAnimationFrame.js"></script>
<script type="text/javascript" src="../js/pannellum.js"></script>
<script type="text/javascript" src="standalone.js"></script>
</body>


Двоични данни
tests/cube.png Целия файл

Преди След
Ширина: 300  |  Височина: 200  |  Големина: 7.0 KiB

Двоични данни
tests/equirectangular.png Целия файл

Преди След
Ширина: 300  |  Височина: 200  |  Големина: 108 KiB

Двоични данни
tests/multires.png Целия файл

Преди След
Ширина: 300  |  Височина: 200  |  Големина: 7.4 KiB

+ 290
- 0
tests/run_tests.py Целия файл

@@ -0,0 +1,290 @@
#!/usr/bin/env python3

"""
Selenium-based test suite for Pannellum

Dependencies:
Python 3, Selenium Python bindings, Pillow, NumPy
Either: Firefox & geckodriver or Chrome & chromedriver

Run tests for Pannellum, set up with Continuous Integration.
Contributed by Vanessa Sochat, JOSS Review 2019.
See the project repository for licensing information.
"""

from random import choice
from threading import Thread
from http.server import SimpleHTTPRequestHandler
from socketserver import TCPServer
import argparse
import io
import os
import re
import subprocess
import sys
import time
import numpy as np
from PIL import Image, ImageChops
from selenium.common.exceptions import TimeoutException
from selenium import webdriver


class PannellumServer(SimpleHTTPRequestHandler):
"""Here we subclass SimpleHTTPServer to capture error messages.
"""

def log_message(self, format, *args):
"""
Log to standard error with a date time string,
and then call any subclass specific logging functions.
"""
sys.stderr.write(
"%s - - [%s] %s\n"
% (self.address_string(), self.log_date_time_string(), format % args)
)

# Workaround for error trying to GET html
if not re.search("div", format % args) and not re.search(
"function", format % args
):
if re.search("404", format % args):
raise IOError(format % args)

def log_error(self, format, *args):
"""Catch errors in the log_messages instead.
"""
pass


class PannellumTester(object):
"""Bring up a server with a testing robot.
"""

def __init__(self, port=None, browser="Chrome", headless=False):
self.handler = PannellumServer
if port:
self.port = port
else:
self.port = choice(range(8000, 9999))
print("Selected port is %s" % self.port)
self.httpd = TCPServer(("", self.port), self.handler)
self.server = Thread(target=self.httpd.serve_forever)
self.server.setDaemon(True)
self.server.start()
self.started = True
self.pause_time = 100
self.browser = None
self.headless = headless
self.display = None
self.driver = browser

def take_screenshot(self, element_id, filename=None):
"""Take a screenshot of an element with a given ID.
"""
element = self.browser.find_element_by_id(element_id)
img = Image.open(io.BytesIO(element.screenshot_as_png)).convert("RGB")
if filename is not None:
img.save(filename)
return img

def equal_images(self, reference, comparator, name, threshold=5):
"""Compare two images, both loaded with PIL, based on pixel differences."""
diff = np.mean(np.array(ImageChops.difference(reference, comparator)))
print("%s difference: %s" % (name, diff))
if diff >= threshold:
comparator.save("tests/" + name + "-comparison.png")
raise ValueError("Screenshot difference is above threshold!")

def run_tests(self, create_ref=False):
"""Run tests for Pannellum."""

print("Loading page...")
self.get_page("http://localhost:%s/tests/tests.html" % self.port)

print("Running tests...")
time.sleep(5)

assert self.browser.execute_script("return viewer.isLoaded()") is True

# Check equirectangular
assert self.browser.execute_script(
"return viewer.getScene() == 'equirectangular'"
)
if create_ref:
self.take_screenshot("panorama", "tests/equirectangular.png")
subprocess.call(
["optipng", "-o7", "-strip", "all", "tests/equirectangular.png"]
)
else:
reference = Image.open("tests/equirectangular.png")
comparator = self.take_screenshot("panorama")
self.equal_images(reference, comparator, "equirectangular")
print("PASS: equirectangular")

# Check movement
self.browser.execute_script("viewer.setPitch(30).setYaw(-20).setHfov(90)")
time.sleep(2)
assert self.browser.execute_script(
"return viewer.getPitch() == 30 && viewer.getYaw() == -20 && viewer.getHfov() == 90"
)
self.browser.find_element_by_class_name("pnlm-zoom-in").click()
time.sleep(1)
assert self.browser.execute_script("return viewer.getHfov() == 85")
self.browser.find_element_by_class_name("pnlm-zoom-out").click()
time.sleep(1)
assert self.browser.execute_script("return viewer.getHfov() == 90")
print("PASS: movement")

# Check look at
self.browser.execute_script("viewer.lookAt(-10, 90, 100)")
time.sleep(2)
assert self.browser.execute_script(
"return viewer.getPitch() == -10 && viewer.getYaw() == 90 && viewer.getHfov() == 100"
)
print("PASS: look at")

# Check cube
self.browser.execute_script("viewer.loadScene('cube')")
time.sleep(5)
assert self.browser.execute_script("return viewer.getScene() == 'cube'")
if create_ref:
self.take_screenshot("panorama", "tests/cube.png")
subprocess.call(["optipng", "-o7", "-strip", "all", "tests/cube.png"])
else:
reference = Image.open("tests/cube.png")
comparator = self.take_screenshot("panorama")
self.equal_images(reference, comparator, "cube")

# Check hot spot
self.browser.find_element_by_class_name("pnlm-scene").click()
time.sleep(5)
assert self.browser.execute_script("return viewer.getScene() == 'multires'")
print("PASS: hot spot")

# Check multires
if create_ref:
self.take_screenshot("panorama", "tests/multires.png")
subprocess.call(["optipng", "-o7", "-strip", "all", "tests/multires.png"])
else:
reference = Image.open("tests/multires.png")
comparator = self.take_screenshot("panorama")
self.equal_images(reference, comparator, "multires")

self.httpd.server_close()

def get_browser(self, name=None):
"""Return a browser if it hasn't been initialized yet.
"""
if name is None:
name = self.driver

log_path = "tests/%s-driver.log" % name.lower()

if self.browser is None:
if name.lower() == "firefox":
fp = webdriver.FirefoxProfile()
fp.set_preference("layout.css.devPixelsPerPx", "1.0")
self.browser = webdriver.Firefox(
service_log_path=log_path, firefox_profile=fp
)
self.browser.set_window_size(800, 600)
else:
options = webdriver.ChromeOptions()
options.add_argument("headless")
options.add_argument("no-sandbox")
options.add_argument("window-size=800x600")
self.browser = webdriver.Chrome(
service_log_path=log_path, options=options
)
return self.browser

def get_page(self, url):
"""Open a particular URL, checking for timeout.
"""
if self.browser is None:
self.browser = self.get_browser()

try:
return self.browser.get(url)
except TimeoutException:
print("Browser request timeout. Are you connected to the internet?")
self.browser.close()
sys.exit(1)

def stop(self):
"""Close any running browser or server and shut down the robot.
"""
if self.browser is not None:
self.browser.close()
self.httpd.server_close()

if self.display is not None:
self.display.close()


def get_parser():
parser = argparse.ArgumentParser(description="Run tests for Pannellum")

parser.add_argument(
"--port",
"-p",
dest="port",
help="Port to run web server",
type=int,
default=None,
)

parser.add_argument(
"--headless",
dest="headless",
help="Start a display before browser",
action="store_true",
default=False,
)

parser.add_argument(
"--create-ref", dest="create_ref", action="store_true", default=False
)

parser.add_argument(
"--browser",
"-b",
dest="browser",
choices=["Firefox", "Chrome"],
help="Browser driver to use for the robot",
type=str,
default="Chrome",
)
return parser


def main():
parser = get_parser()

try:
args = parser.parse_args()
except:
sys.exit(0)

# Add this script's directory, in case it contains driver binaries
here = os.path.abspath(os.path.dirname(__file__))
os.environ["PATH"] = here + ":" + os.environ["PATH"]
os.chdir(here)

# We must be in root directory
os.chdir("..")

# Initialize the tester
tester = PannellumTester(
browser=args.browser, port=args.port, headless=args.headless
)

# Run tests
tester.run_tests(create_ref=args.create_ref)

# Clean up shop!
tester.stop()


if __name__ == "__main__":
main()

+ 88
- 0
tests/tests.html Целия файл

@@ -0,0 +1,88 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pannellum Tests</title>
<link rel="stylesheet" href="../src/css/pannellum.css"/>
<script type="text/javascript" src="../src/js/libpannellum.js"></script>
<script type="text/javascript" src="../src/js/pannellum.js"></script>
<style>
#panorama {
width: 300px;
height: 200px;
}
</style>
</head>
<body>

<div id="panorama"></div>
<script>
viewer = pannellum.viewer('panorama', {
"default": {
"author": "testauthor",
"firstScene": "equirectangular",
"autoLoad": true
},
"scenes": {
"cube": {
"title": "cube title",
"type": "cubemap",
"cubeMap": [
"../examples/multires/1/f00.png",
"../examples/multires/1/r00.png",
"../examples/multires/1/b00.png",
"../examples/multires/1/l00.png",
"../examples/multires/1/u00.png",
"../examples/multires/1/d00.png"
],
"hotSpots": [
{
"pitch": -12,
"yaw": 170,
"type": "info",
"text": "info test"
},
{
"pitch": -10,
"yaw": -50,
"type": "info",
"text": "link test",
"URL": "https://github.com/mpetroff/pannellum"
},
{
"pitch": 0,
"yaw": -10,
"type": "scene",
"text": "scene test",
"sceneId": "multires"
}
]
},

"equirectangular": {
"title": "equirectangular title",
"panorama": "../examples/examplepano.jpg"
},

"multires": {
"title": "multires title",
"type": "multires",
"hfov": 85,
"multiRes": {
"basePath": "../examples/multires",
"path": "/%l/%s%x%y",
"fallbackPath": "/fallback/%s",
"extension": "png",
"tileResolution": 256,
"maxLevel": 4,
"cubeResolution": 2048
}
}
}
});
</script>

</body>
</html>

+ 6
- 3
utils/build/build.py Целия файл

@@ -8,7 +8,6 @@ import urllib.parse

JS = [
'js/libpannellum.js',
'js/RequestAnimationFrame.js',
'js/pannellum.js',
]

@@ -100,7 +99,11 @@ def build(files, css, html, filename, release=False):
if release:
version = read('../VERSION').strip()
else:
version = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('utf-8').strip()
if os.path.exists('../../.git'):
version = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('utf-8').strip()
else:
print('No .git folder detected, setting version to testing')
version = "testing"
js = js.replace('"_blank">Pannellum</a>','"_blank">Pannellum</a> ' + version)
with open('../../src/standalone/standalone.js', 'r') as f:
standalone_js = f.read()
@@ -132,7 +135,6 @@ def build(files, css, html, filename, release=False):
html = merge(html)
html = html.replace('<link type="text/css" rel="Stylesheet" href="../css/pannellum.css"/>','<style type="text/css">' + standalone_css + '</style>')
html = html.replace('<script type="text/javascript" src="../js/libpannellum.js"></script>','')
html = html.replace('<script type="text/javascript" src="../js/RequestAnimationFrame.js"></script>','')
html = html.replace('<script type="text/javascript" src="../js/pannellum.js"></script>','<script type="text/javascript">' + standalone_js + '</script>')
html = html.replace('<script type="text/javascript" src="standalone.js"></script>','')
html = html.replace('<link type="text/css" rel="Stylesheet" href="standalone.css"/>', '')
@@ -143,6 +145,7 @@ def build(files, css, html, filename, release=False):
output(addHeaderJS(js, version), folder + filename)

def main():
os.chdir(os.path.dirname(os.path.abspath(__file__))) # cd to script dir
if (len(sys.argv) > 1 and sys.argv[1] == 'release'):
build(JS, CSS, HTML, 'pannellum', True)
else:


+ 2
- 2
utils/doc/generate.sh Целия файл

@@ -1,7 +1,7 @@
#!/bin/sh

# Generates API documentation.
# Requires documentationjs <http://documentation.js.org/>.
# Requires documentationjs <https://documentation.js.org/>.

# Usage:
#
@@ -19,4 +19,4 @@ elif [ "$1" = "private" ]; then
fi

echo "Generating documentation..."
documentation ../../src/js/pannellum.js ../../src/js/libpannellum.js -o generated_docs -f html --name Pannellum --project-version $version -g $private
documentation build ../../src/js/pannellum.js ../../src/js/libpannellum.js -o generated_docs -f html --project-name Pannellum --project-version $version -g $private

+ 11
- 0
utils/multires/Dockerfile Целия файл

@@ -0,0 +1,11 @@
FROM ubuntu:18.04

# docker build -t generate-panorama .
# docker run -it -v $PWD:/data generate-panorama /data/image.jpg

ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && \
apt-get install -y python3 python3-dev python3-pil hugin-tools

ADD generate.py /generate.py
ENTRYPOINT ["python3", "/generate.py"]

+ 10
- 2
utils/multires/generate.py Целия файл

@@ -47,9 +47,17 @@ except KeyError:
# Handle case of PATH not being set
nona = None

# Subclass parser to add explaination for semi-option nona flag
class GenParser(argparse.ArgumentParser):
def error(self, message):
if '--nona' in message:
sys.stderr.write('''IMPORTANT: The location of the nona utility (from Hugin) must be specified
with -n, since it was not found on the PATH!\n\n''')
super(GenParser, self).error(message)

# Parse input
parser = argparse.ArgumentParser(description='Generate a Pannellum multires tile set from a full or partial equirectangular or cylindrical panorama.',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser = GenParser(description='Generate a Pannellum multires tile set from a full or partial equirectangular or cylindrical panorama.',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('inputFile', metavar='INPUT',
help='panorama to be processed')
parser.add_argument('-C', '--cylindrical', action='store_true',


+ 105
- 0
utils/multires/readme.md Целия файл

@@ -0,0 +1,105 @@
# Generating multi-resolution tiles for a panorama

## Get a Panorama
If you don't have your own, it's easy to use one of the examples in the repository.
From this directory, run:

```bash
$ cp ../../examples/examplepano.jpg .
```

## Generate tiles

To use the `generate.py` script, either its dependencies need to be installed,
or [Docker](https://www.docker.com/) can be used to avoid this installation.

### Option 1: with local dependencies

The `generate.py` script depends on `nona` (from [Hugin](http://hugin.sourceforge.net/)),
as well as Python with the [Pillow](https://pillow.readthedocs.org/) package. On Ubuntu,
these dependencies can be installed by running:

```bash
$ sudo apt install python3 python3-pil hugin-tools
```

Once the dependencies are installed, a tileset can generated with:

```bash
$ python3 generate.py examplepano.jpg
Processing input image information...
Assuming --haov 360.0
Assuming --vaov 180.0
Generating cube faces...
Generating tiles...
Generating fallback tiles...
```


### Option 2: with Docker

A small Dockerfile is provided that allows one to easily generate a panorama tileset
with the [generate.py](generate.py) script, without needing to install dependencies
on one's host.

First, build the Docker container:

```bash
$ docker build -t generate-panorama .
```

When it's finished, you can bind the present working directory to a location in
the container (`/data`) so that your image is found in the container. Notice
that the output needs to be specified in a directory that is bound to the host:

```bash
$ docker run -it -v $PWD:/data generate-panorama --output /data/output /data/examplepano.jpg
Processing input image information...
Assuming --haov 360.0
Assuming --vaov 180.0
Generating cube faces...
Generating tiles...
Generating fallback tiles...
```

## Viewing output (for either method)

The final output will be in your present working directory:

```bash
$ ls output/
1 2 3 config.json fallback
```

Next, change back to the root and start a server:

```bash
$ cd ../..
$ python3 -m http.server
```

A generated tileset and configuration in `utils/multires/output` can then be viewed by navigating a browser to:

[http://localhost:8000/src/standalone/pannellum.htm?config=../../utils/multires/output/config.json](http://localhost:8000/src/standalone/pannellum.htm?config=../../utils/multires/output/config.json)

When the page is loaded, the console will output a logging stream corresponding to the HTTP requests:

```bash
127.0.0.1 - - [09/Aug/2019 09:41:24] "GET /src/standalone/pannellum.htm?config=../../utils/multires/output/config.json HTTP/1.1" 200 -
127.0.0.1 - - [09/Aug/2019 09:41:24] "GET /src/css/pannellum.css HTTP/1.1" 200 -
127.0.0.1 - - [09/Aug/2019 09:41:24] "GET /src/standalone/standalone.css HTTP/1.1" 200 -
127.0.0.1 - - [09/Aug/2019 09:41:24] "GET /src/js/libpannellum.js HTTP/1.1" 200 -
127.0.0.1 - - [09/Aug/2019 09:41:24] "GET /src/js/pannellum.js HTTP/1.1" 200 -
127.0.0.1 - - [09/Aug/2019 09:41:24] "GET /src/standalone/standalone.js HTTP/1.1" 200 -
127.0.0.1 - - [09/Aug/2019 09:41:24] "GET /utils/multires/output/config.json HTTP/1.1" 200 -
127.0.0.1 - - [09/Aug/2019 09:41:24] "GET /src/css/img/background.svg HTTP/1.1" 200 -
127.0.0.1 - - [09/Aug/2019 09:41:24] "GET /src/css/img/sprites.svg HTTP/1.1" 200 -
127.0.0.1 - - [09/Aug/2019 09:41:24] "GET /src/css/img/compass.svg HTTP/1.1" 200 -
127.0.0.1 - - [09/Aug/2019 09:41:26] "GET /src/css/img/grab.svg HTTP/1.1" 200 -
127.0.0.1 - - [09/Aug/2019 09:41:27] "GET /utils/multires/output//1/r0_0.jpg HTTP/1.1" 200 -
127.0.0.1 - - [09/Aug/2019 09:41:27] "GET /utils/multires/output//1/f0_0.jpg HTTP/1.1" 200 -
127.0.0.1 - - [09/Aug/2019 09:41:27] "GET /utils/multires/output//1/u0_0.jpg HTTP/1.1" 200 -
...
```

The panorama, in multi-resolution format, should display in the browser.

Зареждане…
Отказ
Запис