Пишем мэшап с помощью Nokia Maps JS API и Twitter Search API

в 11:42, , рубрики: javascript, Maps API, Nokia Maps API, Nokia Maps JS API, twitter api, Блог компании Nokia, метки: , ,

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

Пишем мэшап с помощью Nokia Maps JS API и Twitter Search API

Мы решили продолжить тему мэшапов на основе Карт Nokia, и сегодня покажем, как с помощью связки Nokia Maps JS API + Twitter Search API отобразить на карте интенсивность использования тех или иных хештегов в Twitter. Выглядеть такой мэшап будет так, как на картинке ниже.

Пишем мэшап с помощью Nokia Maps JS API и Twitter Search API

По традиции начнём с создания 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


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js