LE Blog

Инженер с поэтической душой

11.05.2016 firtree_right Как показать Яндекс Панорамы где угодно

История

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

Со временем, сменился плеер и появился код вставки вида:

<script src="//panoramas.api-maps....."></script>

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

Hack

Параметры

Сначала посмотрим на адрес. Сразу видно две вещи. Никакого уникального идентификатора панорамы в нём не присутствует. Если переходить между панорамами не меняя направление взгляда, и копировать код для вставки на сайт, то различаться будет только один параметр:

ll=37.61782676%2C55.75074572

Это же долгота и широта (в таком порядке) через запятую! Второе, с чем сразу же хочется повозиться — это параметр:

size=690%2C495

Это ширина и высота окна через запятую. Если посмотреть на то, как работает скрипт, и что он оставляет после себя на странице, то совсем не обязательно лезть и деобфусцировать код. Всё понятно: скрипт создаёт вместо себя тег iframe и другой тег script, а себя удаляет. Параметр size и задаёт размеры айфрейма.

Уже этого нам достаточно, чтобы собрать небольшой плеер панорам с картой, который по клике на карте открывает панораму из этого или ближайшего места. Для примера я использую Leaflet. У него такой приятный синтаксис и процесс!

var latlng = L.latLng(55.75074572, 37.61782676);
var $map = L.map('map', {center: latlng, zoom: 15});
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo($map);

function openPanoramaAt(latlng) {
    var panoDiv = $('#panorama');
    var panoScript = document.createElement('script');
    panoScript.type = 'text/javascript';
    panoScript.src = 'https://panoramas.api-maps.yandex.ru/embed/1.x/?lang=ru&ll=' + latlng.lng + '%2C' + latlng.lat + '&ost=dir%3A0.0%2C0.0~span%3A130%2C70.26418362927674&size=' + panoDiv.width() + '%2C' + panoDiv.height() + '&l=stv';
    panoDiv.empty();
    panoDiv[0].appendChild(panoScript);
}

openPanoramaAt(latlng);

$map.on('click', function (me) {
    $map.panTo(me.latlng);
    openPanoramaAt(me.latlng);
});

Кроме лифлета я, конечно, люблю джейквери. Но, к сожалению, на нём невозможно вставить тег script, чтобы он заработал. Поэтому тут немного намешано.

Но нам нужно идти дальше. Ведь внутри панорам можно переходить по стрелкам, а у нас это никак не отображается на карте. Что же делать?

Человек посередине

Чтобы определить, что делает скрипт, у нас и так уже открыт инспектор страницы. Теперь, дорогой читатель, давай переключимся во вкладку «сеть».

Переходя по стрелкам мы увидим, что, кроме всего прочего, плеер запрашивает файлик по адресу https://panoramas.api-maps.yandex.ru/panorama/1.x/?l=stv&lang=ru_RU&...&format=json. Тут уже нет широты и долготы, а присутствует идентификатор, но если открыть этот джейсон в новой вкладке, то внутри него мы увидим нужные нам координаты:

JSON.parse(response).data.Data.Point.coordinates

То, что браузер видит это запрос, означает, что скорее всего в основе лежит XMLHttpRequest. Так как заголовка Content-Security-Policy не видно (по правде сказать, настраивать его довольно сложно, и обычно если кто и прописывает такой заголовок, то там среди прочего есть unsafe-inline), то мы попробуем подслушать, о чём говорит плеер с сервером.

Можно не изобретать велосипед и просто поискать, как это делают уже до нас. В нашем случае плеер находится внутри генерящегося на лету айфрейма и именно класс внутри этого айфрейма нам и надо подменить. Для этого используем модный нынче MutationObserver. Я собрал работающий пример на jsfiddle, и предлагаю его вниманию дорогих читателей.

Сразу видно несколько недостатков:

  1. Размеры панорамы не адаптируются при изменении размеров окна.
  2. Невозможно отследить направление взгляда, чтобы показать его на карте. Я рылся в объектах, но ничего не нашёл.
  3. Пришла беда, откуда не ждали: случился прогресс!

Правильный API

Тема этого топика была запланирована у меня некоторое время назад. И основные фишки были опробованы и сделаны тоже некоторое время назад. Но когда я сел писать конкретный код, то обнаружил, что в стандартной поставке API карт уже есть панорамы. Произошло это в прошлой версии 2.1.38 от 31 марта 2016. Сейчас я работал с 2.1.39. Всего 42 дня как можно ставить панорамы на карты!

Конечно же, я собрал такой же пример на API Яндекс Карт. (Всё-таки синтаксис лифлета намного изящнее, извините). Это настолько новое явление, что даже не входит в стандартный полный набор модулей. Заметьте во внешних ресурсах слева я написал для загрузки (иначе не работает):

load=package.full,panorama.isSupported,panorama.locate,panorama.createPlayer,panorama.Player

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

Так что с точки зрения дальнейшего использования, моё решение утратило свою актуальность. Но с точки зрения образовательной — очень даже отличный материал, я считаю!

Отдельно ссылки:

  1. Как приспособить код для вставки Яндекс Панорам на свои карты (jsfiddle);
  2. Как показать панорамы с помощью API Яндекс Карт (jsfiddle);
  3. Intercept all ajax calls (stackoverflow);
  4. API Яндекс Карт;
  5. Лучший API для картографических сервисов Leaflet.

28.03.2009 firtree_right Автосохранение с помощью JavaScript

О стандартизации

Ещё недавно я говорил о том, что не очень люблю JavaScript за то, что результат работает неодинаково в разных браузерах разных версий и ратовал за Flash и иже с ним, как за прекрасную альтернативу. Но судьба предоставила мне шанс убедиться, что и Flash с его ActionScript вполне может себя вести непредсказуемо в различный браузерах и разных версиях плеера, если программа достаточно сложная.

Я сразу же вспомнил о своём научном руководителе, который в школе курировал мою работу для поступления в ВУЗ. В своей работе он использовал FORTRAN. И на наши недоумённые вопросы отвечал, что это чуть ли не единственный язык, для которого можно записать текст программы любой сложности на дискетту и перенести на любой компьютер любой платформы. Видимо, так оно до сих пор и есть.

Задача

Чтобы отдать дань не очень справедливо заклейменному JavaScript, я решил написать для своего блога и для блога Иры автосохранение.

Ресурсы

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

Решение

Решение оказалось не очень сложное, поэтому я просто его опубликую с дополнительными пояснениями по мере надобности.

config/routes.rb

map.resources :posts, :member => {:autosave => :post}

Контроллер

...
def autosave
  @post = Post.find(params[:id])
  if @post.update_attributes(params[:post])
    render :nothing => true
  else
    render :nothing => true, :status => 400
  end
end
...

Шаблон

Привожу только код JavaScript, который понадобится для сохранения. Рекомендую обратить внимание, на решение проблемы authenticity_token

var cachedBody = $("post_body").value;
var cachedTitle = $("post_title").value;
var saveTimeout = 20000;
var modTimeout = 200;
var requestInProgress = false;

function checkSave() {
  if ((((cachedBody != $("post_body").value) && ($("post_body").value != "")) ||
       ((cachedTitle != $("post_title").value) && ($("post_title").value != ""))) &&
      !requestInProgress) {
    cachedTitle = $("post_title").value;
    cachedBody = $("post_body").value;
    $("spinner").show();
    requestInProgress = true;
    new Ajax.Request("<%= autosave_post_url(@post) -%>", {
        method: "post",
        postBody: Form.serializeElements([$("post_title"), $("post_body")]) +
              '&authenticity_token=' + encodeURIComponent('<%= form_authenticity_token -%>'),
        onSuccess: function (transport) {
              var now = new Date();
              $("spinner").hide();
              $("autosave_text").innerHTML = "Saved at " + 
                    (now.getHours() < 10 ? "0" : "") + now.getHours() + ":" +
                    (now.getMinutes() < 10 ? "0" : "") + now.getMinutes() + ":" +
                    (now.getSeconds() < 10 ? "0" : "") + now.getSeconds();
              setTimeout("checkSave()", saveTimeout);
              requestInProgress = false;
            },
        onFailure: function (transport) {
              $("spinner").hide();
              $("autosave_text").innerHTML = "Failed to autosave";
              requestInProgress = false;
            }
      });
  } else {
    setTimeout("checkSave()", saveTimeout);
  }
}

function checkMod() {
  if (((cachedBody != $("post_body").value) && ($("post_body").value != "")) ||
      ((cachedTitle != $("post_title").value) && ($("post_title").value != ""))) {
    if ($("autosave_text").innerHTML.charAt(0) != "*") {
      $("autosave_text").innerHTML = "* " + $("autosave_text").innerHTML;
    }
  }
  setTimeout("checkMod()", modTimeout);
}

function checks() {
  checkSave();
  checkMod();
}

document.observe('dom:loaded', checks);

В данном случае spinner — это id крутилочки. Крутилочка показывается, когда проходит запрос. Так же здесь имеется дополнительный цикл вызовов. Раз уж мы всё равно храним копию того, что сохранено на сервере, то почему бы не показывать, когда текст уже изменен, но не сохранен на сервере. С помощью звездочки.

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

Материалы для изучения

Интересный пример ненавязчивого JavaScript для создания «бесконечной» страницы