Вставка собственных панорам

Общие сведения

Подготовка панорамного изображения

Нарезка панорамного изображения на тайлы

Создание класса панорамы

Добавление маркеров на панораму

Создание виртуального тура

Добавление стандартных стрелок перехода

Отображение панорамы на странице

Теоретические сведения

Типичные ошибки при создании панорам

Общие сведения

С помощью API плеера панорам на сайте можно отображать собственные панорамы. Например, можно показать панорамные снимки любимого пейзажа или интерьера своего дома. Соединив панорамы между собой, можно создать виртуальный тур, который позволит пользователям подробно ознакомиться с планом местности.

API плеера панорам позволяет:

  • отображать полные сферические панорамы, а также панорамы с неполным вертикальным обзором;
  • отмечать различные объекты на панораме (например, дома или достопримечательности);
  • добавлять на панораму стандартные элементы управления;
  • соединять панорамы между собой и добавлять на них стрелки перехода;
  • добавлять эффект прогрессивного джипега;
  • управлять отображением панорамы на уровне программного кода.

Примечание

API предназначен для показа только сферических панорам с полным горизонтальным обзором.

Чтобы отобразить на странице собственную панораму, следует выполнить следующие шаги:

  1. Подготовить панорамное изображение
  2. Нарезать изображение на тайлы
  3. Создать класс, описывающий панораму
  4. Передать плееру объект панорамы

Подготовка панорамного изображения

Сферические панорамы получаются из отдельных снимков, сделанных из одной точки и затем склеенных с помощью специальной программы. Все снимки нужно делать последовательно, причем область перекрытия идущих рядом снимков должна составлять не менее 20%. Обратите внимание, что панорама должна быть замкнутой по горизонтали, то есть последний снимок в горизонтальном ряду должен перекрываться с первым. Подробные рекомендации по съемке сферических панорам приведены в википедии.

Для подготовки панорамного изображения необязательно использовать профессиональную фотокамеру, можно воспользоваться обычным смартфоном. Большинство современных мобильных приложений позволяют снимать и затем автоматически склеивать полученные снимки в сферические панорамы.

Например, так выглядит панорамное изображение, созданное с помощью стандартного Android-приложения:

Нарезка панорамного изображения на тайлы

После того как панорамное изображение будет готово, его нужно нарезать на тайлы. Плеер будет подгружать на страницу не всю панораму целиком, а по тайлам, причем будут подгружаться только те тайлы, которые попадают в текущую область видимости. Это позволит увеличить скорость загрузки панорамы при ее просмотре.

Тайлы могут принимать форму квадрата или прямоугольника. Форму и размеры тайлов разработчик выбирает самостоятельно. Следует учитывать, что размер тайлов должен быть степенью двойки (например, 512x256 пикселей). Кроме того, панорамное изображение должно быть приведено к такому размеру, чтобы по его ширине вмещалось целое число тайлов (для высоты изображения это условие является необязательным).

Тайлы нужно хранить на сервере в отдельных файлах с расширением JPG или PNG. Для именования файлов можно использовать шаблоны (описание шаблонов можно посмотреть в документации к Активным областям).

Для нарезки изображения на тайлы можно воспользоваться любым удобным графическим редактором. Ниже приведен пример нарезки панорамного изображения на тайлы с помощью редактора ImageMagic. Каждый тайл будет сохранен в отдельном файле с названием «x-y.jpg», где x — номер тайла по оси X, y — номер тайла по оси Y (порядковый отсчет начинается с левого верхнего тайла).

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

Примечание

Перед тем как нарезать изображение на тайлы, следует убедиться, что по ширине изображения умещается целое число тайлов. Если это условие не выполняется, нужно привести изображение к подходящему размеру.

Ниже проиллюстрирован результат разбиения:

Примечание

Обратите внимание, что по вертикали получилось не целое число тайлов. Такое разбиение является допустимым.

Чтобы оптимизировать отображение панорамы, рекомендуется подготовить панорамное изображение нескольких размеров (уровней детализации), например высокого и низкого разрешения. Плеер будет автоматически выбирать наиболее подходящий уровень детализации для текущих размеров DOM-элемента, в котором размещена панорама, а также для текущего поля зрения панорамы.

Для каждого уровня детализации следует подготовить отдельный набор тайлов. При этом размер тайлов для всех уровней детализации должен быть одинаковым. На странице GitHub приведен пример тайлов для двух уровней детализации: в папке hq хранятся тайлы для высокого уровня детализации, в папке lq — для низкого уровня.

Обратите внимание, что созданные уровни детализации не влияют на возможность изменять уровень масштабирования (размеры поля зрения) на панораме. Плеер позволяет изменять уровень масштабирования (с помощью кнопок «+/-» на панораме) даже при одном уровне детализации.

Эффект прогрессивного джипега

Если ширина изображения для самого маленького уровня детализации меньше 2048 пикселей, то этот уровень будет использован для создания эффекта прогрессивного джипега (progressive jpeg). API подгрузит тайлы этого уровня с самым высоким приоритетом и будет показывать их там, где нет тайлов лучшего качества (например, если эти тайлы еще не загружены). Ниже проиллюстрирован пример эффекта прогрессивного джипега.

Создание класса панорамы

Следующим шагом необходимо создать класс, который будет описывать созданное панорамное изображение. В дальнейшем экземпляр этого класса будет передаваться на вход плееру.

Класс панорамы должен реализовывать интерфейс IPanorama, то есть предоставлять определенный набор методов (геттеров), посредством которых плеер будет запрашивать нужную информацию о панораме (URL тайлов, их размеры и пр). Чтобы разработчику не нужно было самостоятельно реализовывать все методы из интерфейса IPanorama, в API создан абстрактный класс panorama.Base. Он реализует базовую функциональность для работы с панорамами, и разработчику достаточно создать свой класс на основе panorama.Base, переопределив некоторые методы (подробнее см. ниже).

Чтобы создать свой класс на основе panorama.Base, нужно воспользоваться методом util.defineClass:

// Создаем свой класс панорамы.
function MyPanorama () { 
  // Вызываем конструктор родительского класса - конструктор panorama.Base.
  ymaps.panorama.Base.call(this);
}

// Устанавливаем наш класс как дочерний для ymaps.panorama.Base.
// Тогда все базовые методы, определенные в классе panorama.Base, будут автоматически
// унаследованы в нашем классе MyPanorama.
ymaps.util.defineClass(MyPanorama, ymaps.panorama.Base, {
  // В классе MyPanorama необходимо переопределить методы (их описание см. ниже):
  getAngularBBox: function () {/*...*/},
  getPosition: function () {/*...*/},
  getTileSize: function () {/*...*/},
  getTileLevels: function () {/*...*/}
  
  // Остальные методы из panorama.Base нужно переопределять при необходимости.
  // Описание этих методов приведено в справочнике.
});

Ниже приведено описание методов, которые нужно переопределить в классе панорамы:

  • getAngularBBox — метод должен возвращать геометрию панорамы. Например: [0.5 * Math.PI, 2 * Math.PI, -0.5 * Math.PI, 0].
  • getPosition — метод должен возвращать позицию панорамы в заданной системе координат. Например: [55.76, 37.64, 0]. По умолчанию используется географическая система координат.
  • getTileSize — метод должен возвращать размер тайлов, на которые порезано панорамное изображение. Пример: [512, 512].
  • getTileLevels — метод должен возвращать объект, содержащий информацию об уровнях детализации изображения. Этот объект должен реализовывать интерфейс IPanoramaTileLevel, то есть должен содержать два метода: get_imagesize и getTileUrl. Пример приведен ниже.

Внимание

Если какой-либо из указанных методов не будет переопределен, при попытке отобразить панораму возникнет ошибка.

Все остальные методы из интерфейса IPanorama следует определять при необходимости (например, если на панораму нужно добавить маркеры).

Ниже приведен полный пример реализации класса панорамы:

// Создаем объект, содержащий информацию о нашей будущей панораме.
var panoramaData = {
      // Геометрия панорамы. 
      angelarBBox: [0.5 * Math.PI, 2 * Math.PI, -0.5 * Math.PI, 0],
      // Позиция.
      position: [55.76, 37.64],
      // Размер тайлов.
      tileSize: [512, 512],
      // Уровни детализации панорамы.
      tileLevels: [{
        // URL тайлов для высокого уровня детализации.
        getTileUrl: function (x, y) {
          return '/tiles/hq/' + x + '/' + y + '.jpg';
        },
        // Размер изображения для высокого уровня детализации.
        get_imagesize: function () {
          return [4096, 2048];
        }
      }, {
        // URL тайлов для низкого уровня детализации.
        getTileUrl: function (x, y) {
          return '/tiles/lq' + x + '/' + y + '.jpg';
        },
        // Размер изображения для низкого уровня детализации.
        get_imagesize: function () {
          return [512, 512];
        }
      }] 
    };   

// Создаем класс панорамы.
function MyPanorama (angularBBox, position, tileSize, tileLevels) { 
  ymaps.panorama.Base.call(this);
  this._angularBBox = angularBBox;
  this._position = position;
  this._tileSize = tileSizel
  this._tileLevels = tileLevels; 
  // Убедимся, что с нашей панорамой все хорошо.
  // Подробнее см. <xref href="#panorama-class/validate"/>.
  this.validate();
}

// Расширяем класс MyPanorama методами из panorama.Base.
ymaps.util.defineClass(MyPanorama, ymaps.panorama.Base, {
  // Для простоты не будем создавать отдельные классы, а просто в каждом методе
  // будем возвращать готовые объекты.
  getAngularBBox: function () {
    return this._angularBBox;
  },
  getPosition: function () {
    return this._position;
  },
  getTileSize: function () {
    return this._tileSize;
  },
  getTileLevels: function () {
    return this._tileLevels;
  }
});  

// Создаем объект панорамы с нужными параметрами.
var panorama = new MyPanorama(
      panoramaData.angularBBox,
      panoramaData.position,
      panoramaData.tileSize,
      panoramaData.tileLevels
    );

// Для отображения панорамы создаем плеер и передаем ему объект панорамы (см. подробнее).
var player = new ymaps.panorama.Player('div_id', panorama);

Примечание

Если на странице нужно отобразить простую панораму (без маркеров и переходов), то для создания объекта панорамы можно воспользоваться вспомогательной функцией panorama.Base.createPanorama. На вход она принимает необходимую информацию о панораме (геометрию, позицию и др.) и на основе этих данных создает объект панорамы. Обратите внимание, что на такую панораму нельзя будет добавить маркеры и задать переходы на другие панорамы.

Проверка на корректность данных созданной панорамы

Прежде чем передавать объект панорамы плееру, рекомендуется проверить корректность данных этой панорамы. Для этого в классе панорамы нужно вызвать метод validate():

function MyPanorama () { 
  ymaps.panorama.Base.call(this);
  // Убедимся, что с нашей панорамой все хорошо.
  this.validate();
}

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

Метод validate() выполняет проверку следующих условий:

  • Панорама должна иметь полный угол обзора по горизонтали;
  • Размер тайлов должен быть степенью двойки;
  • По ширине панорамного изображения должно умещаться целое число тайлов (условие должно выполняться для каждого уровня детализации).

Если одно из этих условий не выполняется, метод генерирует ошибку. Стабильная работа плеера с такой панорамой не гарантируется.

Изменение направления взгляда и размеров поля зрения

По умолчанию, при открытии панорамы в плеере, угол поля зрения устанавливается в 130 на 80 градусов, а направление взгляда задается на север в горизонт. Чтобы изменить эти значения, необходимо в классе панорамы переопределить методы getDefaultSpan и getDefaultDirection соответственно.

Ниже приведен пример переопределения метода getDefaultSpan:

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

ymaps.util.defineClass(MyPanorama, ymaps.panorama.Base, {
  // Уменьшим размеры поля зрения на панораме.
  getDefaultSpan: function () {
    return [30, 30];
  },
  getAngularBBox: function () {/*...*/},
  getPosition: function () {/*...*/},
  getTileSize: function () {/*...*/},
  getTileLevels: function () {/*...*/}
});

Добавление маркеров на панораму

API позволяет добавлять на панорамы специальные визуальные элементы — маркеры. С помощью маркеров на панораме можно отмечать различные объекты, например дома, достопримечательности, автобусные остановки и т. д. Маркеры помогут сориентироваться в пространстве, предоставляя пользователю необходимую информацию, например адреса объектов или направление маршрута.

API не предоставляет стандартных изображений для маркеров, поэтому разработчику необходимо самостоятельно продумать их внешний вид. Изображения маркеров можно хранить на сервере — плеер будет подгружать их по мере необходимости. Также допускается отрисовка маркера на стороне клиента, с помощью технологии Canvas.

Маркер — это интерактивный элемент, который изменяет свое состояние, реагируя на действия пользователя. Чтобы маркер менял свой внешний для разных состояний, необходимо для каждого состояния создать отдельное изображение. Подробнее см. в разделе Состояния маркера.

Чтобы добавить маркер на панораму, нужно:

  1. Создать класс, описывающий маркер. Класс должен реализовывать интерфейс IPanoramaMarker.
  2. Реализовать метод getMarkers в классе панорамы. Этот метод должен возвращать экземпляры класса маркера.
// Класс маркера.
// Класс должен реализовывать интерфейс IPanoramaMarker.
function Marker () {
  // В классе должно быть определено поле properties.
  this.properties = new ymaps.data.Manager();
}

// Записываем в .prototype маркера нужные методы (описание методов см. ниже).
ymaps.util.defineClass(Marker, {
  getIconSet: function () {/*...*/},
  getPosition: function () {/*...*/},
  getPanorama: function () {/*...*/}
});

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

// В классе панорамы нужно переопределить метод getMarkers.
ymaps.util.defineClass(MyPanorama, ymaps.panorama.Base, {
  // Метод getMarkers должен возвращать массив объектов Marker.
  getMarkers: function () {
    return [new Marker ()/*, ...*/];
  },
  getAngularBBox: function () {/*...*/},
  getPosition: function () {/*...*/},
  getTileSize: function () {/*...*/},
  getTileLevels: function () {/*...*/}
});

В классе Marker должно быть определено поле properties, а также три метода: getIconSet, getPosition и getPanorama. Поле properties должно содержать ссылку на объект data.Manager:

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

Ниже приведено описание методов, которые нужно реализовать в классе маркера:

getIconSet

Асинхронный метод. Он должен возвращать объект-Promise, который будет разрешен объектом, содержащим изображения для разных состояний маркера. Объект, описывающий изображения, должен реализовывать интерфейс IPanoramaMarkerIconSet.

Ниже приведены три примера реализации метода getIconSet.

Первый пример демонстрирует, как для разных состояний маркера задать изображения, которые будут отрисованы на Canvas.

// Пример 1. Отрисовка изображений для разных состояний маркера через Canvas.

// Функция renderImage() рисует изображение для нужного состояния маркера.
// Возвращает объект CanvasImageElement.
function renderImage(state) {
  var ctx = document.createElement('canvas').getContext('2d');
  ctx.canvas.width = 250;
  ctx.canvas.height = 32;
  ctx.fillText('Состояние маркера: ' + state, 120, 16);
  return ctx.canvas;
}

// Метод getIconSet возвращает Promise, который будет
// разрешен объектом, содержащим 
// CanvasImageElement-объекты для разных состояний маркера.
Marker.prototype.getIconSet = function () {
  return ymaps.vow.resolve(
    {
      // Состояние 'default' является обязательным.
      'default': {
        image: renderImage('по умолчанию'),
        offset: [0, 0]
      },
      hovered: { 
        image: renderImage('курсор над маркером'),
        offset: [0, 0]
      }/*,
      expanded: { },
      expandedHovered: { }
      */
    }
  );
}

Во втором примере рассмотрен случай, в котором изображение для маркера подгружается с сервера. Изображение задается только для состояния 'default'.

// Пример 2.
// Метод getIconSet возвращает Promise, который будет разрешен
// объектом HTMLImageElement.
Marker.prototype.getIconSet = function () {
 return new ymaps.vow.Promise(function(resolve) {
    var defaultMarkerIcon = new Image();
     // Дожидаемся, когда изображение загрузится с сервера.
     defaultMarkerIcon.onload = function() {
      resolve({
        'default': {
           image: defaultMarkerIcon,
           offset: [64, 16]
        }
      })
    }
    defaultMarkerIcon.src = '/_images/my-icon.png';
  });
}

В третьем примере изображения также подгружаются с сервера, но на этот раз задаются для нескольких состояний маркера.

// Пример 3.
// Функция loadImage возвращает Promise, который будет разрешен объектом
// HTMLImageElement, когда картинка загрузится с сервера.
function loadImage (src) {
  new ymaps.vow.Promise(function (resolve) {
      var image = new Image();
      image.onload = function () {
        resolve(image);
      };
      image.crossOrigin = 'anonymous';
      image.src =  src;
    })
}

// Метод getIconSet возвращает Promise, который будет разрешен
// объектом HTMLImageElement для каждого из четырех состояний.
Marker.prototype.getIconSet =  function () {
  return ymaps.vow.Promise.all([ 
    // defaultSrc - URL изображения для состояния 'default'.
    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]
       }
    };
  });
}

В песочнице приведен подробный пример добавления маркера на панораму. В данном примере изображения задаются для трех состояний маркера: 'default', 'hovered' и 'expanded'. Для первых двух состояний изображение подгружается с сервера, а для состояния 'expanded' изображение рисуется на Canvas.

getPosition

Данный метод должен возвращать координаты маркера (в той системе координат, в которой задана панорама).

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

Примечание

Для упрощения вычисления координат маркера в API реализована статическая функция panorama.Base.getMarkerPositionFromDirection. На вход функция принимает объект панорамы, направление взгляда на маркер и расстояние до маркера. На выходе отдает координаты маркера в той системе координат, в которой задана панорама.

getPanorama

Метод должен возвращать ссылку на панораму, в которой определен маркер.

Примечание

API не позволяет добавлять маркеры на панорамы Яндекса.

Открыть пример в песочнице

Состояния маркера

Маркер — это интерактивный элемент, который меняет свое состояние, реагируя на действия пользователя. Маркер может быть в одном из четырех состояний:

  • обычное состояние ('default');
  • курсор наведен на маркер ('hovered');
  • развернутое состояние ('expanded');
  • развернутое состояние при наведенном курсоре ('expandedHovered').

Смена этих состояний производится по следующей схеме:

Чтобы маркер изменял свой внешний вид в разных состояниях, нужно задать отдельные изображения для этих состояний (изображения задаются в методе getIconSet). При этом необязательно задавать изображения для всех четырех состояний. Например, маркер, обозначающий дома на панорамах Яндекса, не содержит состояния 'expandedHovered'. Остальные его состояния представлены следующим образом:

Внимание

Для состояния 'default' изображение маркера нужно задавать всегда.

Если для какого-либо состояния (отличного от 'default') изображение не задано, то маркер не будет визуально реагировать на соответствующее пользовательское событие.

Создание виртуального тура

Часто возникает задача показать на сайте виртуальный тур — связанные между собой панорамы, переход между которыми осуществляется через специальные элементы управления. Как правило, в виде таких элементов выступают стрелки, показывающие направление перехода между панорамами. Пример стрелок можно посмотреть на панорамах Яндекса.

API плеера позволяет связывать между собой собственные панорамы и добавлять на них стандартные стрелки перехода. Кроме того, в качестве элементов перехода могут выступать специальные маркеры-переходы (см. иллюстрацию ниже). Эти маркеры ведут себя так же, как и обычные маркеры, но при клике на них осуществляется переход на связанную панораму. API не предоставляет стандартных изображений для маркеров-связей, их внешний вид необходимо продумать самостоятельно.

Добавление стандартных стрелок перехода

Чтобы добавить на панораму стандартные стрелки перехода, нужно:

  1. Создать класс, описывающий переход с одной панорамы на другую. Этот класс должен реализовывать интерфейс IPanoramaConnectionArrow.
  2. Реализовать метод getConnectionArrows в классе панорамы. Метод должен возвращать экземпляры класса переходов.

Ниже приведен пример реализации, а после идут объяснения к примеру.

// Создаем класс, описывающий переход с одной панорамы на другую.
// Этот класс должен реализовывать интерфейс IPanoramaConnectionArrow.
function ConnectionArrow () {
  // В классе должно быть определено поле properties.
  this.properties = new ymaps.data.Manager();
}

// Записываем в .prototype класса перехода нужные методы
// (описание методов см. ниже).
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, {
  // Переопределяем метод getConnectionArrows.
  getConnectionArrows: function () {
    return [new ConnectionArrow (), ...]
  },
  getAngularBBox: function () {/*...*/},
  getPosition: function () {/*...*/},
  getTileSize: function () {/*...*/},
  getTileLevels: function () {/*...*/}
});

Класс стрелки перехода (в примере это класс ConnectionArrow) должен содержать поле properties, а также три метода:

getConnectedPanorama

Асинхронный метод. Должен возвращать Promise-объект, который будет разрешен объектом связанной панорамы.

getConnectedPanorama: function () {
  return ymaps.vow.resolve(new MyPanorama(/* Параметры панорамы. */));
}

Также можно задать переход на панораму Яндекса. Для этого метод getConnectedPanorama должен возвращать результат вызова функции panorama.locate, например:

getConnectedPanorama: function () {
  return ymaps.panorama.locate([55.732, 37.584]).then(
    function(panoramas) {
      if (panoramas.length) {
        return panoramas[0];
      } else {
        console.log("Панорама не найдена.");
      }
    }
  );
}
getDirection

Данный метод задает ориентацию стрелок на панораме. Он должен возвращать направление на панораму, на которую осуществляется переход. Направление задается в формате [bearing, pitch], где bearing — азимут направления (в радианах), pitch — угол подъема над линией горизонта (в радианах).

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

Данный метод должен возвращать ссылку на текущую панораму, из которой выполняется переход.

Внимание

При создании перехода с одной панорамы на другую, переход в обратную сторону автоматически не создается. Его необходимо создавать отдельно.

Открыть пример в песочнице

Добавление маркеров-переходов

Маркеры-переходы объединяют в себе функциональность обычных маркеров и стрелок переходов. С помощью маркеров-переходов на панораме можно отметить какой-нибудь объект, а при клике на них перейти на другую панораму. Изображение для этих маркеров нужно создавать самостоятельно.

Примечание

В отличие от обычных маркеров, маркеры-переходы могут находиться только в двух состояниях: 'default' и 'hovered'.

Маркеры-переходы создаются так же, как и обычные маркеры, но с добавлением метода getConnectedPanorama в классе маркера. Кроме того, в классе панорамы нужно реализовать метод getConnectionMarkers.

// Создаем класс, описывающий маркер-переход.
// Этот класс должен реализовывать интерфейс IConnectionMarker,
// с добавлением метода getConnectedPanorama.
function ConnectionMarker () {
  // В классе должно быть определено поле properties.
  this.properties = new ymaps.data.Manager();
}

// Записываем в .prototype маркера-перехода нужные методы. 
ymaps.util.defineClass(ConnectionMarker, {
    // Описание метода getConnectedPanorama см. в классе стрелок перехода.
    getConnectedPanorama: function () {/*...*/},
    // Описание остальных методов см. в классе маркера.
    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 () {/*...*/}
})

Открыть пример в песочнице

Внимание

При создании перехода с одной панорамы на другую, переход в обратную сторону автоматически не создается. Его необходимо создавать отдельно.

Отображение панорамы на странице

Чтобы отобразить панораму на странице, необходимо создать экземпляр плеера (объект panorama.Player). Его конструктору необходимо передать идентификатор DOM-элемента, в который будет встроена панорама, а также сам объект панорамы.

// Создаем плеер с объектом нашей панорамы.
var player = new ymaps.panorama.Player('div_id', panorama, {
      // Тут можно передать опции плеера.
    });

Примечание

Если загрузка панорамы происходит достаточно долго, убедитесь, что для нее создан низкий уровень детализации (с размером не более 3000 пикселей по ширине) и что этот уровень описан в объекте панорамы.

При отображении панорамы на нее автоматически добавляются элементы управления. С их помощью пользователи смогут изменять параметры отображения панорамы (уровень масштабирования, направление взгляда и пр.). Кроме того, API предоставляет набор функций, позволяющих управлять отображением панорамы на уровне кода. Подробнее см. Управление отображением панорамы.

Совместимость с браузерами

Чтобы проверить, будет ли работать плеер панорам в браузере пользователя, можно воспользоваться статической функцией panorama.isSupported():

if (panorama.isSupported()) {
    console.log("API поддерживает данный браузер.");
} else {
    console.log("Данный браузер не поддерживается.");
}

Теоретические сведения

API плеера панорам предназначен для показа только сферических панорам, представленных в равнопромежуточной (эквидистатной) проекции. Для получения таких панорам можно воспользоваться обычным смартфоном. Например, стандартное Android-приложение позволяет снимать и сразу склеивать панорамы в нужной проекции.

Для отображения панорамы на странице плеер выполняет специальные преобразования, в результате которых получается проекция панорамного изображения на внутреннюю часть сферы. При просмотре панорамы в плеере взгляд будет направлен из центра сферы.

Чтобы изображение было спроецировано на сферу без искажений, плееру необходимо передать корректную геометрию панорамы. Геометрия задает границы панорамы на сфере, а также определяет ориентацию панорамы в пространстве. Геометрию панорамы разработчик должен вычислять самостоятельно (подробнее см. ниже).

Геометрия панорамы

Геометрия панорамы задается координатами правого верхнего и левого нижнего углов прямоугольника, ограничивающего панорамное изображение на сфере. На рисунке ниже эти углы обозначены точками A и B:

Координаты этих точек задаются в сферической системе координат и определяются следующим образом:

  • координата θ\theta — это угол между плоскостью экватора и лучом, проведенным из центра сферы в заданную точку (см. рис. 2). Для панорам с полным вертикальным обзором координата θ\theta должна быть всегда равной π2\frac{\pi}{2} для верхней точки (точка A) и π2-\frac{\pi}{2} для нижней точки (точка B). Для панорам с неполным вертикальным обзором θ\theta может принимать значения в промежутке (π2...π2)(-\frac{\pi}{2}...\frac{\pi}{2}) см. подробнее.
  • координата ϕ\phi — это азимутальный угол, то есть угол смещения плоскости, в которой изображение состыкуется на сфере, от направления на север на панораме (см. рис. 2). Угол отсчитывается по часовой стрелке. Направление на север на панораме разработчику необходимо определить самостоятельно.

Примечание

Координата rr (расстояния от точки до центра сферы) не используется.

Примечание

Координата θ\theta задает вертикальный угол обзора панорамы; координата ϕ\phi задает ориентацию панорамы в пространстве.

Обозначим координаты точки A как (θT,ϕR)(\theta_{T}, \phi_{R}), а точки B как (θB,ϕL)(\theta_{B}, \phi_{L}). На рисунке 3 проиллюстрировано, как определить координаты этих точек.

Внимание

При вычислении координат θ\theta и ϕ\phi следует учитывать следующие требования:

  1. Координата ϕR\phi_R всегда должна вычисляться по формуле: ϕR=ϕL+2π\phi_R=\phi_L+2\pi.
  2. Для координат θT\theta_T и θB\theta_B должно выполняться равенство: θTθB=hpxwpx2π\theta_T-\theta_B=\frac{h_{px}}{w_{px}}2\pi, где hpxh_{px} — высота изображения в пикселях; wpxw_{px} — ширина изображения в пикселях (можно взять изображение любого уровня детализации).

Чтобы упростить расчет геометрии, можно предположить, что линия стыка панорамного изображения на сфере совпадает с линией направления на север (см. рис. 4). Тогда ϕL=0\phi_L=0, а ϕR=2π\phi_R=2\pi. Обратите внимание, что если в действительности направление на север на панораме не совпадает с линией стыка изображения, то при отображении в плеере на панораме будут некорректные геодезические направления.

Геометрию панорамы нужно задавать в класса панорамы, в методе getAngularBBox. Метод должен возвращать геометрию в виде массива, содержащего последовательность координат точек A и B, то есть [θT,ϕR,θB,ϕL][\theta_{T}, \phi_{R}, \theta_{B}, \phi_{L}]. Например, для полной сферической панорамы метод должен возвращать [fracπ2frac{\pi}{2}, 00, fracπ2-frac{\pi}{2}, 2π2\pi].

Геометрия панорамы с неполным вертикальным обзором

API позволяет отображать панорамы с неполным вертикальным обзором.

Вертикальные границы панорамы на сфере задаются координатами θT\theta_T и θB\theta_B. Они могут принимать значения в промежутке (π2...π2)(-\frac{\pi}{2}...\frac{\pi}{2}).

Внимание

Для расчета координат θ\theta можно воспользоваться следующей формулой : θTθB=hpxwpx2π\theta_T-\theta_B=\frac{h_{px}}{w_{px}}2\pi, где hpxh_{px} — высота изображения в пикселях; wpxw_{px} — ширина изображения в пикселях (можно взять изображение любого уровня детализации).

Рассмотрим пример из песочницы, в котором отображена панорама с неполным вертикальным обзором. На рис. 5 проиллюстрирована проекция этой панорамы на сферу:

Как видно из рисунка, панорама расположена ниже относительно экватора (такая ситуация возникла из-за того, что при съемке панорамы камера была смещена вниз по отношению к горизонтальной оси). Поэтому для этой панорамы были подобраны следующие значения: θT=π10\theta_T=\frac{\pi}{10} и θB=π5\theta_B=-\frac{\pi}{5}. Если изменить эти значения, например, задать θT=π5\theta_T=\frac{\pi}{5} и θB=π9\theta_B=-\frac{\pi}{9} (то есть «поднять» границы панорамы), то при отображении на панораме появятся сильные искажения — панорама будет проваливаться в линии горизонта (проделайте этот эксперимент в песочнице).

Подробнее о том, в каких случаях возникают те или иные искажения на панораме, см. в разделе Типичные ошибки при задании геометрии.

Типичные ошибки при задании геометрии

Ниже приведены типичные ошибки, которые приводят к сильным искажениям при отображении панорамы.

  • Если на панораме видна линия стыка или панорама состыкована некорректно, значит координата ϕR\phi_{R} не равна ϕL+2π\phi_{L}+2\pi.

  • Если панорама «проваливается» в линии горизонта, то это означает, что координаты θT\theta_T и θB\theta_B заданы выше, чем реальное расположение панорамы на сфере. Например, так будет выглядеть панорама из примера, если для нее задать θT=0.25π\theta_T=0.25\pi и θB=0.05π\theta_B=-0.05\pi:

    В этом случае границы панорамы нужно опустить вниз, например, θT=0.1π\theta_T=0.1\pi и θB=0.2π\theta_B=-0.2\pi.

  • Если линия горизонта на панораме вогнута, это означает, что координаты θT\theta_T и θB\theta_B заданы ниже, чем реальные границы панорамы на сфере. Например, так будет выглядеть панорама из примера, если для нее задать θT=0.25π\theta_T=-0.25\pi и θB=0.55π\theta_B=-0.55\pi:

    В этой ситуации, наоборот, нужно поднять границы изображения, увеличив значения координат θT\theta_T и θB\theta_B.

  • Если панорама сжимается или растягивается по горизонтали, это означает, что не выполняется равенство θTθB=hpxwpx2π\theta_T-\theta_B=\frac{h_{px}}{w_{px}}2\pi.

    В этом случае следует подобрать другие значения θT\theta_T и θB\theta_B, при которых данное равенство будет выполняться.