Embedding custom panoramas

General information

You can use the Panoramas API to display your own panoramas on a website. For example, you can show panoramic views of your favorite landscape or the interior of your home. You can stitch panoramas together to create a virtual tour that will allow users to explore the area.

The Panoramas API lets you:

  • Display fully spherical panoramas, as well as panoramas that lack a full vertical view.
  • Mark objects on the panorama (such as houses or landmarks).
  • Add standard controls to the panorama.
  • Connect panoramas to each other and add connection arrows that link them.
  • Add the progressive JPEG effect.
  • Control the panorama image at the code level.

Note

The API is designed for showing spherical panoramas with a full horizontal view.

To display your own panorama on a page, follow these steps:

  1. Prepare the panoramic image
  2. Divide the image into tiles
  3. Create a class that describes the panorama
  4. Pass the panorama object to the panorama player

Preparing the panoramic image

Spherical panoramas are made from separate pictures taken from the same point and then stitched together using a special program. You need to take all the pictures in order, and the overlap where pictures will meet should be at least 20%. Note that the panorama must meet up horizontally, meaning the last image in a horizontal line must overlap the first one. For recommendations on shooting spherical panoramas, see this Wikipedia page.

You don't have to use a professional camera to prepare the panorama image. You can use a normal smartphone. Most modern mobile apps allow you to take photos and then automatically stitch them together into spherical panoramas.

For example, this is what a panorama image looks like that was created using the standard Android app:

Dividing the panoramic image into tiles

Once the panoramic image is ready, you need to cut it into tiles. The panorama player doesn't load the entire panorama on the page at once. It loads the panorama by tiles, and it only loads the tiles that fill within the current viewing area. This makes the panorama load faster when it is viewed.

Tiles can be in the shape of a square or a rectangle. The shape and size of tiles are chosen by the developer. Note that the tile size must be a power of two (for example, 512x256 pixels). In addition, the panoramic image must be sized so that a whole number of tiles fit into its width (this isn't a requirement for the image height).

Store the tiles on the server in separate files with the JPG or PNG extension. For file naming, you can use the templates built into the API (see the documentation on hotspots for a description of the templates).

To cut the image into tiles, you can use any convenient image editor. The example below shows cutting the panoramic image into tiles using the ImageMagic program. Each tile will be saved in a separate file called “ x-y.jpg ”, where x is the tile number on the X axis, and y is the tile number on the Y axis (coordinates start from the upper-left corner of the image).

$ convert panorama.jpg -crop 512x512 -set filename:tile "%[fx:page.x/512]-%[fx:page.y/512]" "tiles/%[filename:tile].jpg"

Note

Before you divide the image into tiles, make sure that a whole number of tiles will fit into its width. If this requirement isn't met, you need to change the size of the image.

The example below shows the result of dividing into tiles:

Note

Note that the vertical dimension doesn't accommodate a whole number of tiles. This is acceptable.

To optimize the panorama display, we recommend preparing several sizes of the panorama image (with different levels of detail), to be used for high and low resolutions. The panorama player automatically selects the best detail level for the current size of the DOM element that the player is in and for the panorama's current field of view.

You should prepare a separate set of tiles for each detail level. However, the tile size should be the same for all the detail levels. The GitHub page shows an example of tiles for two detail levels: the hq folder stores tiles with high resolution, and the lq folder has tiles with low detail.

Note that the created levels of detail do not affect the ability to change the zoom level (the size of the field of view) on the panorama. The panorama player allows the user to change the zoom level using the «+/-» buttons on the panorama even when there is only a single level of detail.

Progressive JPEG effect

If the image width for the lowest level of detail is less than 2048 pixels, this level will be used for creating a progressive JPEG effect. The player will load the tiles for this level first and display them in places where higher-quality tiles aren't available (for example, if the tiles haven't loaded yet). The example below shows a progressive JPEG.

Creating the panorama class

The next step is to create a class that describes the panorama image you have created. Later, an instance of this class will be passed to the panorama player.

The panorama class must implement the IPanorama interface, meaning it provides a specific set of methods (getters) that the player will use to request the necessary information about the panorama (tile URLs, sizes, and so on). The API has an abstract panorama.Base class so the developer doesn't have to implement all the methods from the IPanorama interface. This class implements the basic functionality for working with panoramas. The developer just needs to create a class based on panorama.Base and redefine some of the methods (for more information, see below).

To create your own class based on panorama.Base, use the util.defineClass method:

// Creating a panorama class.
function MyPanorama () { 
  // Calling the parent class constructor - the panorama.Base constructor.
  ymaps.panorama.Base.call(this);
}

// Setting our class as a descendent of ymaps.panorama.Base.
// This means all the base methods defined in the panorama.Base class will be automatically
// inherited in our MyPanorama class.
ymaps.util.defineClass(MyPanorama, ymaps.panorama.Base, {
  // In the MyPanorama class, we need to redefine methods (they are described below):
  getAngularBBox: function () {/*...*/},
  getPosition: function () {/*...*/},
  getTileSize: function () {/*...*/},
  getTileLevels: function () {/*...*/}
  
  // The other methods from panorama.Base can be redefined as necessary.
  // These methods are documented in the reference guide.
});

The methods below need to be redefined in the panorama class:

  • getAngularBBox — This method should return the panorama geometry. Example: [0.5 * Math.PI, 2 * Math.PI, -0.5 * Math.PI, 0].
  • getPosition — This method should return the panorama's position in the specified coordinate system. Example: [55.76, 37.64]. The geographical coordinate system is used by default.
  • getTileSize — This method should return the size of the tiles that the panorama image was divided into. Example: [512, 512].
  • getTileLevels — This method should return the object that contains information about the image's levels of detail. This object must implement the IPanoramaTileLevel interface, meaning it should contain two methods: getImageSize and getTileUrl. An example is provided below.

Alert

If one of these methods isn't redefined, an error will be returned when trying to display the panorama.

All of the other methods from the IPanorama interface should be redefined when they are needed (for example, if you need to add markers to the panorama).

A complete example of implementing the panorama class is shown below:

// Creating an object that contains information about our future panorama.
var panoramaData = {
      // Geometry of the panorama. 
      angelarBBox: [0.5 * Math.PI, 2 * Math.PI, -0.5 * Math.PI, 0],
      // Position.
      position: [55.76, 37.64],
      // Tile size.
      tileSize: [512, 512],
      // Levels of detail on the panorama.
      tileLevels: [{
        // URLs of tiles for high detail level.
        getTileUrl: function (x, y) {
          return '/tiles/hq/' + x + '/' + y + '.jpg';
        },
        // Image size for high detail level.
        getImageSize: function () {
          return [4096, 2048];
        }
      }, {
        // URLs of tiles for low level of detail.
        getTileUrl: function (x, y) {
          return '/tiles/lq' + x + '/' + y + '.jpg';
        },
        // Image size for low detail level.
        getImageSize: function () {
          return [512, 512];
        }
      }] 
    };   

// Creating the panorama class.
function MyPanorama (angularBBox, position, tileSize, tileLevels) { 
  ymaps.panorama.Base.call(this);
  this._angularBBox = angularBBox;
  this._position = position;
  this._tileSize = tileSizel
  this._tileLevels = tileLevels; 
  // Checking our panorama to make sure everything is correct.
  // For more information, see <xref href="#panorama-class/validate"/>.
  this.validate();
}

// Extending the MyPanorama class with methods from panorama.Base.
ymaps.util.defineClass(MyPanorama, ymaps.panorama.Base, {
  // To keep it simple, we aren't creating separate classes. We'll just return 
  // prepared objects in each method.
  getAngularBBox: function () {
    return this._angularBBox;
  },
  getPosition: function () {
    return this._position;
  },
  getTileSize: function () {
    return this._tileSize;
  },
  getTileLevels: function () {
    return this._tileLevels;
  }
});  

// Creating a panorama object with the necessary parameters.
var panorama = new MyPanorama(
      panoramaData.angularBBox,
      panoramaData.position,
      panoramaData.tileSize,
      panoramaData.tileLevels
    );

// Creating the panorama player and passing the parameter to it in order to display the panorama (more information).
var player = new ymaps.panorama.Player('div_id', panorama);

Note

If you want to display a basic panorama on a page (without markers or connections), you can use the panorama.Base.createPanorama auxiliary function to create the panorama object. It accepts the necessary panorama information (the geometry, position, and so on) and creates a panorama object based on this data. Note that you won't be able to add markers to this type of panorama or set connections to other panoramas.

Validating the data in a created panorama

Before passing the panorama object to the panorama player, you should check the validity of the panorama data. To do this, call the validate() method in the panorama class:

function MyPanorama () { 
  ymaps.panorama.Base.call(this);
  // Checking our panorama to make sure everything is correct.
  this.validate();
}

ymaps.util.defineClass(MyPanorama, ymaps.panorama.Base, {
  //...
})

The validate() method checks the following:

  • The panorama must have a full horizontal viewing angle.
  • The tile size must be a power of two.
  • A whole number of tiles must fit the width of the panorama image (this condition must be met for each level of detail).

If one of these conditions isn't met, the method generates an error. The panorama player might not function correctly if the panorama doesn't meet these conditions.

Changing the viewing direction and the size of the field of view

By default, when the panorama player opens, the angle of the field of view is set to 130 by 80 degrees, and the viewing direction is set to north at the horizon. To change these values, redefine the getDefaultSpan and getDefaultDirection methods in the panorama class.

The example below shows how to redefine the getDefaultSpan method:

function MyPanorama () { 
  ymaps.panorama.Base.call(this);
  this.validate();
}

ymaps.util.defineClass(MyPanorama, ymaps.panorama.Base, {
  // Narrowing the field of view on the panorama.
  getDefaultSpan: function () {
    return [30, 30];
  },
  getAngularBBox: function () {/*...*/},
  getPosition: function () {/*...*/},
  getTileSize: function () {/*...*/},
  getTileLevels: function () {/*...*/}
});

Adding markers to a panorama

The API lets you add visual markers to panoramas. You can use markers to identify various objects, such as houses, landmarks, bus stops, and so on. Markers can help users understand what they are looking at and give them valuable information such as addresses or the direction of a transit route.

The API does not provide standard images for markers, so the developer is responsible for designing them. Marker images can be stored on the server, and the panorama player will load them when they are needed. Markers can also be rendered on the client side using Canvas technology.

A marker is an interactive element that changes its state in response to the user's actions. In order to change the marker's appearance for different states, a separate image must be created for each state. For more information, see Marker states.

To add a marker to the panorama:

  1. Create a class that describes the marker. The class must implement the IPanoramaMarker interface.
  2. Implement the getMarkers method in the panorama class. This method must return instances of the marker class.
// Marker class.
// The class must implement the IPanoramaMarker interface.
function Marker () {
  // The properties field must be defined in the class.
  this.properties = new ymaps.data.Manager();
}

// Adding the necessary methods to the marker's .prototype (methods are described below).
ymaps.util.defineClass(Marker, {
  getIconSet: function () {/*...*/},
  getPosition: function () {/*...*/},
  getPanorama: function () {/*...*/}
});

function MyPanorama () {
  ymaps.panorama.Base.call(this);
  this.validate();
} 

// The getMarkers method has to be redefined in the panorama class.
ymaps.util.defineClass(MyPanorama, ymaps.panorama.Base, {
  // The getMarkers method must return an array of Marker objects.
  **getMarkers: function () {**
    return [new Marker ()/*, ...*/];
  },
  getAngularBBox: function () {/*...*/},
  getPosition: function () {/*...*/},
  getTileSize: function () {/*...*/},
  getTileLevels: function () {/*...*/}
});

The Marker class must define the properties field, along with three methods: getIconSet, getPosition and getPanorama. The properties field must contain a reference to the data.Manager object:

function Marker () {
  this.properties = new ymaps.data.Manager();
}

The methods below need to be implemented in the marker class:

getIconSet

Asynchronous method. It must return a Promise object that will be resolved by an object containing images for different marker states. The object that describes the images must implement the IPanoramaMarkerIconSet interface.

Three examples of implementing the getIconSet method are shown below.

The first example demonstrates how to define images that will be drawn on a canvas for the different marker states.

// Example 1. Using Canvas to draw images for various marker states.

// The renderImage() function draws an image for the desired marker state.
// It returns the CanvasImageElement object.
function renderImage(state) {
  var ctx = document.createElement('canvas').getContext('2d');
  ctx.canvas.width = 250;
  ctx.canvas.height = 32;
  ctx.fillText('Marker state: ' + state, 120, 16);
  return ctx.canvas;
}

// The getIconSet method returns a Promise, which will be
// resolved by an object that contains 
// CanvasImageElement objects for different marker states.
Marker.prototype.getIconSet = function () {
  return ymaps.vow.resolve(
    {
      // The 'default' state is required.
      'default': {
        image: renderImage('by default'),
        offset: [0, 0]
      },
      hovered: { 
        image: renderImage('cursor on the marker'),
        offset: [0, 0]
      }/*,
      expanded: { },
      expandedHovered: { }
      */
    }
  );
}

The second example shows a case when the marker image is loaded from the server. The image is only defined for the 'default' state.

// Example 2.
// The getIconSet method returns a Promise, which will be resolved
// by an HTMLImageElement object.
Marker.prototype.getIconSet = function () {
 return new ymaps.vow.Promise(function(resolve) {
    var defaultMarkerIcon = new Image();
     // Waiting for the image to load from the server.
     defaultMarkerIcon.onload = function() {
      resolve({
        'default': {
           image: defaultMarkerIcon,
           offset: [64, 16]
        }
      })
    }
    defaultMarkerIcon.src = '/images/my-icon.png';
  });
}

In the third example, the images are also loaded from the server, but this time they are defined for multiple marker states.

// Example 3.
// The loadImage function returns a Promise, which will be resolved by the
// HTMLImageElement object when the image loads from the server.
function loadImage (src) {
  new ymaps.vow.Promise(function (resolve) {
      var image = new Image();
      image.onload = function () {
        resolve(image);
      };
      image.crossOrigin = 'anonymous';
      image.src =  src;
    })
}

// The getIconSet method returns a Promise object that will be resolved
// by an HTMLImageElement object for each of the four states.
Marker.prototype.getIconSet =  function () {
  return ymaps.vow.Promise.all([ 
    // defaultSrc - Image URL for the 'default' state.
    loadImage(defaultSrc),
    loadImage(hoveredSrc),
    loadImage(expandedSrc)
    loadImage(expandedHoveredSrc)
  ]).spread(function (defaultImage, hoveredImage, expandedImage, expandedHoveredImage) {
    return {
      'default': {
         image: defaultImage,
         offset: [0, 0]
       },
       hovered: {
         image: hoveredImage,
         offset: [0, 0]
       },
       expanded: {
         image: expandedImage,
         offset: [0, 0]
       },
       expandedHovered: {
         image: expandedHoveredImage,
         offset: [0, 0]
       }
    };
  });
}

The sandbox has a detailed example of adding markers to a panorama. In this example, the images are defined for three marker states: 'default', 'hovered' and 'expanded'. For the first two states, the image is loaded from the server, but the 'expanded' image is drawn on the canvas.

getPosition

This method returns the marker coordinates (in the coordinate system that is set for the panorama).

Marker.prototype.getPosition = function () {
  return [2.35, 1, 0];
};

Note

To simplify coordinate calculation for markers, the API has the panorama.Base.getMarkerPositionFromDirection static function. This function accepts the viewing direction for the marker and the distance to it. It outputs the marker coordinates in the coordinate system that is set for the panorama.

getPanorama

This method returns a reference to the panorama where the marker is defined.

Note

The API does not allow you to add markers to Yandex panoramas.

Open the sandbox example

Marker states

A marker is an interactive element that changes its state in response to the user's actions. A marker can have one of four states:

  • normal ('default')
  • cursor on the marker ('hovered')
  • maximized ('expanded')
  • maximized with the cursor on it ('expandedHovered')

The marker switches between these states as follows:

In order for the marker's appearance to change for different states, separate images must be set for these states (the images are set in the getIconSet method). However, you don't have to set images for all four states.

Alert

The marker image must always be set for the 'default' state.

If the image isn't set for one of the other states (other than 'default'), the marker won't visually respond to the corresponding user action.

Creating a virtual tour

Virtual tours are a popular feature for websites. A virtual tour is a set of panoramas connected to each other that have special controls for moving between them. These controls are usually shown as arrows that point in the direction to move between panoramas. You can see an example of arrows on Yandex panoramas.

The panorama player API allows you to connect your own panoramas to each other and add standard connection arrows to them. In addition, you can use special connection markers as controls (see the image below). These markers behave like normal markers, but they take the user to a connected panorama when clicked. The API does not provide standard images for connection markers, so the developer is responsible for designing them.

Adding standard connection arrows

To add standard connection arrows to a panorama:

  1. Create a class that describes the connection from one panorama to another. This class must implement the IPanoramaConnectionArrow interface.
  2. Implement the getConnectionArrows method in the panorama class. The method must return instances of the connection class.

The example below shows the implementation, followed by explanations.

// Creating a class that describes the connection from one panorama to another one.
// This class must implement the IPanoramaConnectionArrow interface.
function ConnectionArrow () {
  // The "properties" field must be defined in the class.
  this.properties = new ymaps.data.Manager();
}

// Adding the necessary methods to the .prototype of the connection class
// (see method descriptions below).
ymaps.util.defineClass(ConnectionArrow, {
  getConnectedPanorama: function () {/*...*/},
  getDirection: function () {/*...*/},
  getPanorama: function () {/*...*/}
});

function MyPanorama () {
  ymaps.panorama.Base.call(this);
  this.validate();
}

ymaps.util.defineClass(MyPanorama, ymaps.panorama.Base, {
  // Redefining the getConnectionArrows method.
  **getConnectionArrows: function () {**
    return [new ConnectionArrow (), ...]
  },
  getAngularBBox: function () {/*...*/},
  getPosition: function () {/*...*/},
  getTileSize: function () {/*...*/},
  getTileLevels: function () {/*...*/}
});

The connection arrow class (in the example, the ConnectionArrow class) must contain the properties field and three methods:

getConnectedPanorama

Asynchronous method. Returns a Promise object that is resolved by the connected panorama object.

getConnectedPanorama: function () {
  return ymaps.vow.resolve(new MyPanorama(/* Panorama parameters. */));
}

You can also create a connection to a Yandex panorama. To do this, the getConnectedPanorama method must return the result of calling the panorama.locate function. For example:

getConnectedPanorama: function () {
  return ymaps.panorama.locate([55.732, 37.584]).then(
    function(panoramas) {
      if (panoramas.length) {
        return panoramas[0];
      } else {
        console.log("Panorama not found.");
      }
    }
  );
}
getDirection

This method sets the orientation of arrows on the panorama. It should return the direction to the panorama to connect to. The direction is defined in the format [bearing, pitch], where bearing is the azimuth of the direction in radians, and pitch is the angle of elevation above the horizon in radians.

getDirection: function () {
 return [47.40, 0];
}
getPanorama

This method should return a reference to the current panorama that the connection is from.

Alert

When you create a connection from one panorama to another, the connection in the opposite direction is not created automatically. You have to create it separately.

Open the sandbox example

Adding connection markers

Connection markers combine the functionality of normal markers and connection arrows. You can use connection markers on a panorama to point out an object and switch to another panorama when it is clicked. You need to create the images for these markers yourself.

Note

Unlike "plain" markers, connection markers can only have two states: 'default' and 'hovered'.

Connection markers are created the same way as normal markers, except you have to add the getConnectedPanorama method to the marker class. You also have to implement the getConnectionMarkers method in the panorama class.

// Creating a class that describes the connection marker.
// This class must implement the IConnectionMarker interface,
// plus the getConnectedPanorama method.
function ConnectionMarker () {
  // The "properties" field must be defined in the class.
  this.properties = new ymaps.data.Manager();
}

// Adding the necessary methods to .prototype for the connection marker. 
ymaps.util.defineClass(ConnectionMarker, {
    // For a description of the getConnectedPanorama method, see the connection arrow class.
    getConnectedPanorama: function () {/*...*/},
    // For descriptions of other methods, see the connection marker class.
    getIconSet: function () {/*...*/},
    getPosition: function () {/*...*/},
    getPanorama: function () {/*...*/}
});

function MyPanorama () {
  ymaps.panorama.Base.call(this);
}

ymaps.util.defineClass(MyPanorama, ymaps.panorama.Base, {
    // Переопределяем метод getConnectionMarkers.
    **getConnectionMarkers: function () {**
        return [new ConnectionMarker (), ...]
    }, 
    getAngularBBox: function () {/*...*/},
    getPosition: function () {/*...*/},
    getTileSize: function () {/*...*/},
    getTileLevels: function () {/*...*/}
})

Open the sandbox example

Alert

When you create a connection from one panorama to another, the connection in the opposite direction is not created automatically. You have to create it separately.

Displaying a panorama on a page

To display a panorama on a page, you have to create a panorama player instance (the panorama.Player object). Pass its constructor the ID of the DOM element that the panorama will be embedded in, along with the panorama object itself.

// Creating the panorama player with our panorama object.
var player = new ymaps.panorama.Player('div_id', panorama, {
      // Pass player options here.
    });

Note

If the panorama is taking too long to load, make sure that it has a low level of detail created for it (with a width less than 3000 pixels), and that this level is defined in the panorama object.

When a panorama is displayed, UI controls are automatically added to it. The controls allow users to change the panorama display settings (the zoom, viewing direction, and so on). In addition, the API offers a set of functions that you can use to control the panorama image at the code level. For more information, see Controlling the panorama display.

Browser compatibility

To check whether the panorama player will work in the user's browser, you can use the static function panorama.isSupported():

if (panorama.isSupported()) {
    console.log("The API supports this browser.");
} else {
    console.log("This browser is not supported.");
}

Background theory

The Panoramas API is only intended for displaying spherical panoramas represented in the equirectangular (equidistant cylindrical) projection. You can use a normal smartphone to capture the panoramas. For example, there is a standard Android app for taking panorama photos and immediately stitching them together in the desired projection.

To display panoramas on a page, the panorama player performs conversions that result in projecting the panorama image onto the inside surface of a sphere. When the panorama is viewed in the player, the viewpoint is directed from the center of the sphere.

To avoid distortion when the image is projected onto the sphere, you must pass the correct panorama geometry to the panorama player. The geometry sets the panorama's borders on the sphere, and also defines the panorama's orientation. The developer is responsible for calculating the panorama geometry (see details below).

Panorama geometry

The panorama geometry is set using the coordinates of the upper-right and lower-left corners of the rectangle that bounds the panoramic image on a sphere. On the image below, these corners are indicated with points A and B:

The coordinates of these points are set in the spherical coordinate system and defined as follows:

  • The θ\theta coordinate is the angle between the equator plane and a ray extending from the center of the sphere to the specified point (see fig. 2). For panoramas with a full vertical view, the θ\theta coordinate must always be equal to π2\frac{\pi}{2} for the upper point (point A) and π2-\frac{\pi}{2} for the lower point (point B). For panoramas without a full vertical view, θ\theta can take values in the range (π2...π2)(-\frac{\pi}{2}...\frac{\pi}{2}) (more information).
  • The ϕ\phi coordinate is the azimuth angle, meaning the angle between the plane where the image meets on the sphere and the direction of North on the panorama (see fig. 2). The angle is measured in a clockwise direction. The developer is responsible for defining North on the panorama.

Note

The rr coordinate (the distance from the point to the center of the sphere) is not used.

Note

The θ\theta coordinate sets the vertical angle of the panorama view; the ϕ\phi coordinate sets the panorama's orientation.

We'll denote the coordinates of point A as (θT,ϕR)(\theta_{T}, \phi_{R}), and point B as (θB,ϕL)(\theta_{B}, \phi_{L}). Figure 3 shows how to define the coordinates of these points.

Alert

When calculating the coordinates of θ\theta and ϕ\phi, note the following requirements:

  1. The ϕR\phi_R coordinate should always be calculated using the formula ϕR=ϕL+2π\phi_R=\phi_L+2\pi.
  2. The θT\theta_T and θB\theta_B coordinates must always use the equation θTθB=hpxwpx2π\theta_T-\theta_B=\frac{h_{px}}{w_{px}}2\pi, where hpxh_{px} is the image height in pixels, and wpxw_{px} is the image width in pixels (you can use an image from any detail level).

To simplify the geometry calculations, you can assume that the line where the panorama image meets itself on the sphere is the same as the line identifying North (see fig. 4). Then ϕL=0\phi_L=0, and ϕR=2π\phi_R=2\pi. Note that if North on the panorama doesn't actually correspond to the seam of the image, the panorama will have incorrect geodetic directions when displayed in the panorama player.

Set the panorama geometry in the panorama class, in the getAngularBBox method. This method should return the geometry as an array containing a sequence of coordinates for points A and B, i.e. [θT,ϕR,θB,ϕL][\theta_{T}, \phi_{R}, \theta_{B}, \phi_{L}]. For example, for a fully spherical panorama, the method should return [fracπ2frac{\pi}{2}, 00, fracπ2-frac{\pi}{2}, 2π2\pi].

Geometry of a panorama without a full vertical view

The API allows you to display panoramas without a full vertical view.

The vertical boundaries of a panorama on the sphere are set using the coordinates θT\theta_T and θB\theta_B. They can take values in the range (π2...π2)(-\frac{\pi}{2}...\frac{\pi}{2}).

Alert

For calculating the θ\theta coordinates, you can use the following formula: θTθB=hpxwpx2π\theta_T-\theta_B=\frac{h_{px}}{w_{px}}2\pi, where hpxh_{px} is the image height in pixels, and wpxw_{px} is the image width in pixels (you can use an image from any detail level).

Let's look at an example from the sandbox that displays a panorama with an incomplete vertical view. Fig. 5 shows the projection of this panorama on a sphere:

As the picture shows, the panorama is positioned lower relative to the equator (this happened because the camera was lowered in relation to the horizontal axis while shooting the panorama). So for this panorama, we selected the following values: θT=π10\theta_T=\frac{\pi}{10} and θB=π5\theta_B=-\frac{\pi}{5}. If we were to change these values, such as set θT=π5\theta_T=\frac{\pi}{5} and θB=π9\theta_B=-\frac{\pi}{9} (in other words, «raise» the panorama's boundaries), significant distortion would appear when displaying the panorama, and the panorama would sink into the horizon (you can try this experiment in the sandbox).

For more information about what causes different types of distortion on a panorama, see Typical mistakes when defining the geometry.

Typical mistakes when defining the geometry

There are some typical mistakes that cause significant distortion when displaying the panorama.

  • If the seam is visible on the panorama, or the panorama is not stitched together correctly, it means the ϕR\phi_{R} coordinate is not equal to ϕL+2π\phi_{L}+2\pi.

  • If the panorama «sinks» into the horizon, this means the θT\theta_T and θB\theta_B coordinates are set higher than the panorama's actual position on the sphere. For example, this is how the panorama from the example will look if we set θT=0.25π\theta_T=0.25\pi and θB=0.05π\theta_B=-0.05\pi for it:

    In this case, the panorama's boundaries need to be lowered. For example, θT=0.1π\theta_T=0.1\pi and θB=0.2π\theta_B=-0.2\pi.

  • If the horizon is concave, this means that the θT\theta_T and θB\theta_B coordinates are set lower than the panorama's actual boundaries on the sphere. For example, this is how the panorama from the example will look if we set θT=0.25π\theta_T=-0.25\pi and θB=0.55π\theta_B=-0.55\pi for it:

    In this situation, on the contrary, we need to raise the boundaries of the image by increasing the values of the θT\theta_T and θB\theta_B coordinates.

  • If the panorama is squeezed together or stretched out horizontally, this means that the equation θTθB=hpxwpx2π\theta_T-\theta_B=\frac{h_{px}}{w_{px}}2\pi isn't satisfied.

    In this case, choose different values for θT\theta_T and θB\theta_B that allow the equation to be satisfied.