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

    На прошлой неделе Карты 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-скрипта. Первый скрипт 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,
    		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();
    		}
    	};
    
    	// Убираем танцующего котика, потому что мы готовы показать оверлей
    	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,
    		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


    Microsoft Lumia
    Company
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 2

      0
      Справедливости ради стоит отметить, что на первом скриншоте (с Flickr) для карт используется JS-библиотека Leaflet, от Нокии только данные.
        +2
        Три раза нажимал «Обновить», чтобы поугарать над прелоадером.

        Only users with full accounts can post comments. Log in, please.