В последнее время разработка расширений для Хрома так упростилась, что я решился наконец поставить галочку против одной из самых долгоживущих в моем ежедневнике задач: доставать из картинок на страницах GEO-таги, прицеплять картинкам title с местом, где фотография была сделана, и давать возможность в один клик глянуть на карту. Кроме того, на страницах с большим количеством фотографий имеет смысл показывать карту со всеми маркерами и предоставлять возможность перейти непосредственно к фотографии по клику на маркер.
Вот как это выглядит на моем сайте, куда я складываю кратенькие фотоотчеты о поездках (для друзей и родственников):
В современном мире на создание такого расширения у меня ушло около трех часов. Расширение доступно в Webstore, исходники традиционно лежат на гитхабе…
Итак, начнем с создания скаффолда (я сверялся с календарем, 2014 год на дворе, с нуля писать не модно).
Подготовка
npm install -g yo generator-chrome-extension
mkdir mycoolext && cd $_
yo chrome-extension
Это нам скачает генератор для расширений Хрома и запустит его:
Отвечаем сообразно здравому смыслу, ждем некоторое время и на выходе имеем удобный проект, управляемый Grunt. Тесты, конечно, придется писать самому, но grunt debug
с поддержкой горячего релоадинга расширения и grunt build
, создающего пакет, пригодный для загрузки в Webstore — мы получили из коробки. Не забудем качнуть зависимости:
npm install
bower install
Манифест
Начнем с правки манифеста. Он не такой длинный, приведу его полностью, с комментариями.
{
"name": "__MSG_extName__", /* мы ❤ l10n */
"description": "__MSG_extDescription__", /* мы ❤ l10n */
"version": "1.0.0", /* каждый вызов grunt build будет увеличивать минор на 1 */
"manifest_version": 2, /* обязательно */
"default_locale": "en", /* обязательно, если мы ❤ l10n */
"icons": {
"16": "icons/16.png",
"48": "icons/48.png",
"128": "icons/128.png"
},
"background": {
"scripts": [
"scripts/chromereload.js", /* горячий релоадинг */
"scripts/background.js" /* наш исполняемый скрипт */
]
},
"page_action": {
"default_icon": {
"16": "icons/16.png",
"19": "icons/19.png",
"38": "icons/38.png",
"48": "icons/48.png",
"128": "icons/128.png"
},
"default_title": "__MSG_extName__",
"default_popup": "popup.html" /* я не использую popup, но пусть будет для наглядности */
},
"permissions": [
"contextMenus",
"tabs",
"storage",
"geolocation", /* расширению это не нужно, в демонстрационных целях */
"http://*/*",
"https://*/*"
],
"content_scripts": [
{
"matches": [
"http://*/*",
"https://*/*"
],
"js": [
"bower_components/jquery/dist/jquery.min.js", /* да, я тащу свою jQuery, я ламер */
"lib/jquery.exif.js", /* плагин для доставания exif http://blog.nihilogic.dk/ */
"lib/leaflet.js", /* скрипт карт от OpenMap http://leafletjs.com/ */
"scripts/main.js" /* мой код */
],
"css": [
"lib/leaflet.css" /* картам нужны стили */
]
}
],
"minimum_chrome_version": "16.0.0.0", /* для полярников и космонавтов, не видящих интернет */
"web_accessible_resources": [
"bower_components/jquery/dist/jquery.min.map", /* я не планирую отлаживать jQuery, но кто знает */
"icons/maps.png", /* иконка «карта» */
"lib/images/*" /* маркеры и прочие картинки для leaflet */
],
"options_page": "options.html" /* страница настроек */
}
Картинки, которые мы хотим отображать на чужих страницах (и скрипты, которые мы хотим подгружать), должны быть явням образом объявлены в соответствующих секциях. Приступим к кодированию.
После загрузки страницы мы пройдемся по картинкам, если они нормального размера — попытаемся вытащить из них exif, оттуда GEO-теги и (в случае успеха) — нарисуем рамку вокруг таких картинок, пропатчим их title и выведем все найденные картинки на карту, которая будет открываться по клику на маленькую иконку, появившуюся в правом верхнем углу страницы. Приступим (с этого момента текст заметки продолжается в комментариях к коду, так проще и явно понятнее что к чему относится).
Обработка exif
$('img').each(function(index, image) {
if (($(image).width() < 100) && ($(image).height() < 100)) { // слишком маленькая
$(image).attr('exif', false);
return true;
}
$(image).exifLoad(function() {
if (! $(image).attr('exif')) return;
// [CUT] тут кусок кода, который долго и муторно достает широту и долготу
// и рассовывает по fLat, fLon, sLat, sLon
// первые два — дробные, вторые — строки типа 53°20′18″N,37°5′18″E
$(image).attr('data-gps-latitude', fLat);
$(image).attr('data-gps-longitude', fLon);
$(image).attr('data-gps-latitude-pretty', sLat);
$(image).attr('data-gps-longitude-pretty', sLon);
// сейчас мы создадим анкор внутри страницы, чтобы на маркер можно было поставить ссылку
var hash = 'img_' + Date.now();
$('<a>').attr('id', hash).insertBefore($(image));
// XHR из расширения дозволено только `background.js`, потому пляски с бубном
chrome.runtime.sendMessage(
{ method: 'getAddressByLatLng', id: counter, lat: sLat, lon: sLon },
function(response) {
var datas = JSON.parse(response.results).response.GeoObjectCollection;
// [CUT] тут кусок кода, который парсит ответ и достает оттуда адрес точки,
// где была сделана фотография
// к этой функции мы еще вернемся
handleLeaflet(iconsize, fLat, fLon, address ? address : sLat + ' ' + sLon, hash);
}
);
// нарисуем вокруг нашей картинки border (цвета задаются в настройках)
$(image).css({
'border-color': color,
'border-width': width,
'border-style': 'solid'
});
});
});
Вроде, все прокомментировал. Пора заглянуть в handleLeaflet
.
var exifSpyMap = exifSpyMap || null;
var exifSpyMarkers = exifSpyMarkers || [];
function handleLeaflet(iconsize, fLat, fLon, tooltip, hash) {
if(!document.getElementById('expifspy-icon-mudasobwa-id')) {
// [CUT] тут создаем и обеспечиваем стилями/свойствами иконку
icon.addEventListener('click', function() {
var leaflet = document.getElementById('expifspy-leaflet-mudasobwa-id');
if(leaflet) {
// leaflet умеет корректно рендерить карту только на видимом (display !== 'none') контроле
leaflet.style.right = leaflet.style.right === '-10000px' ?
(+iconsize - Math.floor(+iconsize / 8)) + 'px' : '-10000px';
}
}, false);
document.body.appendChild(icon);
}
if(!document.getElementById('expifspy-leaflet-mudasobwa-id')) { /* create div to draw leaflet */
// [CUT] тут создаем и обеспечиваем стилями/свойствами карту
leaflet.style.right = '-10000px';
document.body.appendChild(leaflet);
}
if(!exifSpyMap) { // ленивое создание экземпляра карты
L.Icon.Default.imagePath = chrome.extension.getURL('lib/images');
exifSpyMap = L.map('expifspy-leaflet-mudasobwa-id').setView([fLat, fLon], 13);
// добавляем слой с благодарностью авторам
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo(exifSpyMap);
}
// создаем маркер
var marker = L.marker([fLat, fLon]).addTo(exifSpyMap).bindPopup(tooltip);
// по наведению мыши он будет показывать адрес
marker.on('mouseover', function(/*e*/) {
this.openPopup();
});
marker.on('mouseout', function(/*e*/) {
this.closePopup();
});
// по клику — будет проматывать страницу к фотографии
if(hash) {
marker.on('click', function(/*e*/) {
location.hash = '#' + hash;
});
}
// перерендерим карту, чтобы все маркеры попали
exifSpyMarkers.push(L.latLng(fLat, fLon));
exifSpyMap.fitBounds(L.latLngBounds(exifSpyMarkers));
}
Уф. Осталось разобраться с получением адреса по координатам. У гугла какая-то мутная политика, я хожу в Яндекс.
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
switch(message.method) {
// [CUT] показано только важное
case 'getAddressByLatLng':
var url = 'http://geocode-maps.yandex.ru/1.x/?lang=en-US&format=json&geocode='+message.lat+','+message.lon;
var xmlHttpReq = new XMLHttpRequest();
if(xmlHttpReq) {
xmlHttpReq.open('GET', url);
xmlHttpReq.onreadystatechange = function () {
if(xmlHttpReq.readyState === 4 && xmlHttpReq.status === 200) {
sendResponse( { results: xmlHttpReq.responseText } );
}
};
xmlHttpReq.send(null); // 'null', ибо 'GET'
}
break;
}
return true;
});
Сводя воедино
Я не стану приводить код для изменения и хранения опций (все есть на github, плюс он тривиален). Плагин готов, можно тестировать.
$ grunt debug
Running "debug" task
Running "jshint:all" (jshint) task
✔ No problems
Running "concurrent:chrome" (concurrent) task
Running "connect:chrome" (connect) task
Started connect web server on http://localhost:9000
Running "watch" task
Waiting...
>> File "app/scripts/main.js" changed.
Running "jshint:all" (jshint) task
✔ No problems
Done, without errors.
Execution Time (2014-10-21 12:05:41 UTC)
loading tasks 3ms ▇▇ 2%
jshint:all 154ms ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 98%
Total 157ms
Completed in 1.172s at Tue Oct 21 2014 14:05:42 GMT+0200 (CEST) - Waiting...
Можно сходить на страницу, содержащую картинки с гео-тегами и полюбоваться на карту.
В продакшн!
$ grunt build
Running "clean:dist" (clean) task
Cleaning dist/_locales...OK
Cleaning dist/background.html...OK
Cleaning dist/bower_components...OK
Cleaning dist/lib...OK
Cleaning dist/manifest.json...OK
Cleaning dist/options.html...OK
Cleaning dist/popup.html...OK
Cleaning dist/scripts...OK
Cleaning dist/styles...OK
Running "chromeManifest:dist" (chromeManifest) task
Build number has changed to 1, 0, 2
# ............. ⇛ еще тонна отладочного вывода
Running "compress:dist" (compress) task
Created package/exifspy-1.0.2.zip (90771 bytes)
Done, without errors.
Execution Time (2014-10-21 12:45:23 UTC)
clean:dist 104ms ▇▇▇▇ 3%
useminPrepare:html 73ms ▇▇▇ 2%
concurrent:dist 1.1s ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 30%
uglify:dist/scripts/background.js 47ms ▇▇ 1%
uglify:dist/bower_components/jquery/dist/jquery.min.js 993ms ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 28%
uglify:dist/lib/jquery.exif.js 101ms ▇▇▇▇ 3%
uglify:dist/lib/leaflet.js 979ms ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 27%
compress:dist 68ms ▇▇▇ 2%
Total 3.6s
Файл package/exifspy-1.0.2.zip
готов и ждет отправки в Webstore. Если что-то упустил — потормошите, добавлю.
Автор: mudasobwa