Writing a mashup using Nokia Maps JS API and Twitter Search API

Last week, Nokia Maps were integrated into the most popular Flickr photo hosting, resulting in an interesting mashup where you can see photos with geotagged tags on the map.



We decided to continue the topic of mashups based on Nokia Maps, and today we will show how to use the Nokia Maps JS API + Twitter Search API to display the intensity of the use of certain hashtags on Twitter on a map. Such a mashup will look like the image below.



By tradition, we will start by creating index.html, in which our map will be initialized:
<!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>


As you can see, we immediately identified three javascript scripts. You may already be familiar with the from a api.maps.nokia.com/2.2.1/jsl.js previous post - it loads the Nokia Maps JS API.

The script places-heatmap.js is responsible for drawing and adding an intensity map to the overlay map. The following script process-tweets.js searches for tweets with a given hashtag and containing geolocation data, as well as the subsequent geocoding of these tweets with the entry of information about them (latitude / longitude, city) in the data structure of the intensity map.

Explanations for these scripts will be given directly in the comments to the code.

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();


You nokia.maps.heatmap.Overlay can read more about the class used nokia.maps.heatmap.Overlay on the Nokia Maps API Reference nokia.maps.heatmap.Overlay website , however, in the comments to the code, all parameters were listed, not including some settings for the overlay itself, which are set via 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();


When working with the Twitter Search API, it should be borne in mind that although it is possible to set an arbitrary radius in it (even equal to the radius of the Earth), in which it is worth looking for tweets, in the issue it gives no more than 100 tweets. Thus, it is better to indicate the coordinates of many points, otherwise a large number of messages will drop out of the search.

In the end, process-tweets.js you may notice features that are not directly related to the Twitter Search API. They are responsible for the interface of our map and allow clicking on the current hashtag (in the upper left corner) to determine a new one for the search. Through document.getElementsByTagName('h2')[0].innerHTML = '#' + window.location.hash.substring(1); we define the hashtag through the URL - this way our index.html can be inserted on any site as an iframe using any hashtag.

View source


See a live example here . Sources can be downloaded from github .

API Materials Used