На прошлой неделе Карты Nokia были интегрированы в самый популярный фотохостинг Flickr, в результате чего получился интересный мэшап, где на карте можно посмотреть фотографии с проставленными геотегами.
Мы решили продолжить тему мэшапов на основе Карт Nokia, и сегодня покажем, как с помощью связки Nokia Maps JS API + Twitter Search API отобразить на карте интенсивность использования тех или иных хештегов в Twitter. Выглядеть такой мэшап будет так, как на картинке ниже.
По традиции начнём с создания index.html, в котором будет инициализироваться наша карта:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Nokia Maps Heatmap demo</title>
<link rel="stylesheet" href="main.css">
</head>
<body>
<div id="annotations">
<h2></h2>
</div>
<div id="map" class="map"><div id="map-loading" class="map-loading"></div></div>
<script src="http://api.maps.nokia.com/2.2.1/jsl.js?with=all"></script>
<script src="places-heatmap.js"></script>
<script src="process-tweets.js"></script>
</body>
</html>
Как вы видите, мы сразу определили три javascript-скрипта. Первый скрипт http://api.maps.nokia.com/2.2.1/jsl.js
вам уже может быть знаком по прошлому посту — он подгружает Nokia Maps JS API.
Скрипт places-heatmap.js
отвечает за отрисовку и добавление на карту оверлея карты интенсивности. Следующий скрипт process-tweets.js
занимается поиском твитов с заданным хештегом и содержащих геолокационные данные, а также последующим геокодированием этих твитов с занесением информации о них (широта/долгота, город) в структуру данных карты интенсивности.
Пояснения к этим скриптам будут даваться прямо в комментариях к коду.
places-heatmap.js
var HH = {};
// Инициализируем настройки для работы с Nokia Maps JS API
nokia.Settings.set("appId", "_peU-uCkp-j8ovkzFGNU");
nokia.Settings.set("authenticationToken", "gBoUkAMoxoqIWfxWA5DuMQ");
nokia.Settings.set("defaultLanguage", "ru-RU");
HH.HeatmapLoader = function () {
var self = this,
map,
mapLoad,
heatmapLoad,
heatmapProvider;
// Создаём статическую карту, как мы делали в предыдущем посте
mapLoad = function () {
var mapContainer = document.getElementById("map");
self.map = new nokia.maps.map.Display(mapContainer, {
// Центрируем карту примерно над Москвой, хотя при масштабировании zoomLevel: 3 это имеет мало смысла
center: [55, 37],
zoomLevel: 3,
components: [
new nokia.maps.map.component.Behavior()
]
});
};
// Настраиваем оверлей карты интенсивности, затем рисуем его поверх карты
heatmapLoad = function () {
var color_range = {
// Задаём цвета для определенных значений плотности данных.
// Точки с максимальной плотностью равны 1, с минимальной — 0.
stops: {
// Выставляем малые значения градиента, так как имеем дело с картой мира — ОНА ОГРОМНА, ВСЕ ЗНАЧЕНИЯ НА ЕЁ ФОНЕ НИЧТОЖНЫ
"0": "rgba(0, 0, 64, 1)",
"0.15": "rgba(0, 0, 64, 1)",
"0.3": "rgb(32, 32, 96)",
"0.4": "rgb(96, 96, 128)",
"0.5": "rgb(255, 255, 255)"
},
// Включаем интерполяцию между обозначенными значениями градиента, чтобы сделать его плавным
interpolate: true
};
try {
if(!self.heatmapProvider) {
// Создаём оверлей карты интенсивности
heatmapProvider = new nokia.maps.heatmap.Overlay({
// Присваиваем цвета для карты
colors: color_range,
// Максимальный уровень масштаба, для которого отрисовывается оверлей
max: 20,
// Общий уровень прозрачности, применимый к оверлею
opacity: 1,
// Определяем тип карты плотности
type: "density",
// Определяем разрешение создаваемому оверлею карты интенсивности
coarseness: 1,
// Заполняем территорию, не имеющую данных, цветом, определенным для минимального значения
assumeValues: true
});
}
} catch (e) {
// Конструктор оверлея карты интенсивности выдаёт сигнал исключения,
// если браузер не имеет поддержки canvas
alert(e);
}
// Начинаем передачу данных только в случае успешного создания оверлея карты интенсивности
if (heatmapProvider && HH.tweetheatmap) {
// Передаём данные для карты интенсивности
heatmapProvider.clear();
heatmapProvider.addData(HH.tweetheatmap.allPlaces);
// Рендерим карту интенсивности на карту
self.map.overlays.add(heatmapProvider);
}
};
// Определяем публичные методы для объекта
return {
map: map,
mapLoad: mapLoad,
heatmapLoad: heatmapLoad,
heatmapProvider: heatmapProvider
};
};
// Создаём экземпляр HeatmapLoader
HH.heatmap = new HH.HeatmapLoader();
Подробнее про использующийся класс nokia.maps.heatmap.Overlay
можно почитать на сайте Nokia Maps API Reference, однако в комментариях к коду были перечислены все параметры, не считая не которых настроек самого оверлея, которые задаются через nokia.maps.heatmap.Overlay.Options.
process-tweets.js
HH.TweetHeatmap = function () {
"use strict";
var self,
init,
pageSetup,
switchInput,
changeHash,
allPlaces = [],
addPlaces,
addSearch,
tweetPlace,
getLocation,
addToPlace,
futureCheck,
futureCount = 0,
rendered = false,
getLeader,
locationsObj = {},
locationsTweets = [],
displayHeatmap;
init = function () {
var locations, i;
self = this;
// Сразу же отобразим простую карту, чтобы пользователь не лицезрел пустую страницу
if (nokia.maps && HH.heatmap) {
HH.heatmap.mapLoad();
}
// Если хештег не обозначен, выставим хештег #nokia
if (window.location.hash === '') {
window.location.hash = 'nokia';
}
pageSetup();
// Для использования Twitter Search API необходимо обозначить географические координаты, в определенном радиусе которых будут искаться твиты
locations = [[55.75697, 37.61502], [0, 100], [0, 50], [0, 0], [0, -50], [0, -100], [0, -150], [50, 150], [50, 100], [50, 50], [50, 0], [50, -50], [50, -100], [50, -150], [-50, 150], [-50, 100], [-50, 50], [-50, 0], [-50, -50], [-50, -100], [-50, -150]];
// Отобразим гифку с котиком, который будет танцевать, пока грузятся твиты
document.getElementById('map-loading').style.display = 'block';
// Пройдёмся по списку точек, определенных в locations, и найдём все твиты через Twitter Search API для каждой точки
for (i in locations) {
self.addSearch(locations[i], window.location.hash.substring(1));
}
// Если у пользователя медленное соединение и все твиты не успевают подгрузиться,
// насильно отобразим всё, что есть, через восемь секунд
setTimeout(displayHeatmap, 8000);
};
// Сделаем JSONP-запрос с указанным хештегом и локацией, используя Twitter Search API
// Не забудем указать колбек addPlaces
addSearch = function (location, hashtag) {
// Про Twitter Search API можно почитать тут: https://dev.twitter.com/docs/api/1/get/search
var url = 'http://search.twitter.com/search.json?geocode=' + location[0] + ',' + location[1] + ',8000km&q=%23' + hashtag + '&rpp=100&callback= HH.tweetheatmap.addPlaces',
script = document.createElement("script");
script.setAttribute("src", url);
document.body.appendChild(script);
};
// Пройдёмся через все полученные данные, отбирая геолокационные данные для каждого твита
addPlaces = function (data) {
var i;
if (data && data.results && data.results.length) {
// Увеличиваем число ожидаемых запросов.
self.futureCount += data.results.length;
for (i = data.results.length - 1; i >= 0; i--) {
var location = data.results[i].location
if (location) {
location = location.replace('iPhone: ','')
self.getLocation(location);
} else {
// Если данный вызов не может быть геокодирован, уменьшаем число ожидаемых запросов
self.futureCount--;
}
};
}
};
// Делаем JSONP-вызов к Nokia Maps geocode API для полученного через Twitter названия места с целью получения координат
getLocation = function (location) {
// q — название точки, vi — параметр отображения, dv — название клиента, to — число точек в ответе
var url = 'http://where.desktop.mos.svc.ovi.com/json?q=' + encodeURI(location) + '&to=1&vi=address&dv=NokiaMapsAPI&callback_func=HH.tweetheatmap.addToPlace',
script = document.createElement("script");
script.setAttribute("src", url);
document.body.appendChild(script);
};
// Если мы удачно геокодировали этот твит, добавляем
// координаты в структуру данных карты интенсивности
addToPlace = function (data) {
if (data.results && data.results.length) {
var location_title = data.results[0].properties.title,
type = data.results[0].properties.type,
lon = data.results[0].properties.geoLongitude,
lat = data.results[0].properties.geoLatitude;
if (type != 'Country' && type != 'State' && type != 'Continent'){
if (locationsObj[location_title]) {
locationsTweets[locationsObj[location_title]].tweets += 1;
} else {
locationsObj[location_title] = locationsTweets.length
locationsTweets.push({
'city': location_title,
'tweets': 1,
'longitude': lon,
'latitude': lat
});
}
}
if (!rendered) {
allPlaces.push({
"latitude" : lat,
"longitude" : lon,
"city" : location_title,
"country" : data.results[0].properties.addrCountryName
});
}
}
self.futureCheck();
};
// Если все асинхронные вызовы вернули ответ, рисуем карту интенсивности.
// В противном случае, уменьшаем число запросов и начинаем заново
futureCheck = function () {
self.futureCount--;
if (self.futureCount<=0) {
displayHeatmap();
}
};
// Получение топа по использованию хештега среди городов
getLeader = function () {
var arr = locationsTweets.slice()
arr.sort(function(a,b){
return b.tweets - a.tweets
})
return arr.slice(0,3)
};
// Убираем танцующего котика, потому что мы готовы показать оверлей
displayHeatmap = function() {
if(!rendered) {
rendered = true;
document.getElementById('map-loading').style.display = 'none';
HH.heatmap.heatmapLoad();
}
};
// Функции, связанные с лейаутом и функциональностью страницы и не имеющие отношения к карте
switchInput = function(e){
this.style.display='none';
var h = document.createElement('input');h.setAttribute('type', 'text');
this.parentNode.insertBefore(h,this);
h.focus();
h.addEventListener('keydown', changeHash, false);
};
changeHash = function(e){
if(e.keyCode===13) {
window.location.hash='#'+e.target.value.replace('#','');
} else if(e.keyCode===27) {
e.target.parentNode.removeChild(e.target);
document.getElementsByTagName('h2')[0].style.display='block';
}
};
pageSetup = function() {
if (!(document.getElementsByTagName('body')[0].classList.length === 1)) {
// Выставляем хэштег на основе хэша в URL
document.getElementsByTagName('h2')[0].innerHTML = '#' + window.location.hash.substring(1);
// Добавляем event listener для возможности ввести новый хештег
document.getElementsByTagName('h2')[0].addEventListener('click', switchInput, false)
// Добавляем event listener для перезагрузки страницы после ввода нового хештега
window.addEventListener("hashchange", function (e) {window.location.reload(); }, false);
}
};
// Определяем публичные методы для объекта
return {
init: init,
addSearch: addSearch,
addPlaces : addPlaces,
addToPlace : addToPlace,
getLocation: getLocation,
futureCount : futureCount,
futureCheck : futureCheck,
allPlaces : allPlaces,
getLeader : getLeader,
locationsTweets : locationsTweets
};
};
HH.tweetheatmap = new HH.TweetHeatmap();
HH.tweetheatmap.init();
При работе с Twitter Search API стоит учитывать, что в нём хоть и можно задать произвольный радиус (даже равный радиусу Земли), в котором стоит искать твиты, в выдаче он отдаёт не более 100 твитов. Таким образом, лучше указать координаты множества точек, иначе из поиска выпадет большое количество сообщений.
В конце process-tweets.js
вы могли заметить функции, не имеющие прямого отношения к Twitter Search API. Они отвечают за интерфейс нашей карты и позволяют по клику на текущий хештег (в левом верхнем углу) определить для поиска новый. Через document.getElementsByTagName('h2')[0].innerHTML = '#' + window.location.hash.substring(1);
мы определяем хештег через URL — таким образом наш index.html можно вставлять на любой сайт как iframe, используя любой хештег.
Посмотреть исходники
Посмотреть живой пример можно здесь. Исходники можно скачать с github.
Использующиеся материалы API
Автор: nokiaman