IT

เรียก Instagram API ดึงภาพรอบพิกัด lat, lng ด้วย JavaScript

7 October 2015

[อัพเดท 2018: บทความนี้ผมเขียนไว้หลายปีแล้ว อาจใช้ไม่ได้ในปัจจุบัน]

ตอนที่ผมเรียนคอร์ส udacity FEND มี project นึงที่ผมเรียกภาพจาก instagram API มาแสดง วันนี้เอา code ส่วนนั้นมาแชร์กันครับ ผิดถูกประการใดแนะนำด้วยนะครับ ^.^ เรื่อง promise นี่ผมก็ยังไม่ค่อยเข้าใจนัก

Project

Project ที่จะทำในวันนี้หน้าตาเป็นแบบนี้ครับ

ig-project-01

มี spec คือ เมื่อคลิกที่ชื่อ location ที่เราต้องการ จะไปดึงภาพ instagram ที่อยู่รอบๆ latitude, longtiude นั้นมา โดยภาพที่จะดึงได้ เจ้าของภาพต้องตั้งเป็น public (เราไม่สามารถดึงภาพของ private account มาไม่ได้ครับ)

ตัวอย่าง

Instagram API

ก่อนอื่นเลยเราต้องสมัครใช้งาน instagram API ก่อนครับที่เว็บ Instagram Developer สำหรับเรื่องนี้ พี่ DayDev เขียนไว้ละเอียดกว่าผม ลองเข้าไปอ่านกันได้ที่ Instagram API เบื้องต้นกับการดึงรูปภาพมาแสดงโดยใช้ Tags อย่างง่าย หรือใครอยากอ่านภาษาอังกฤษ ลองไปที่ Tutsplus ได้ครับ Introduction to the Instagram API

ig-project-02

อธิบายคร่าวๆ นะครับ คือ Instagram API จะมี endpoint url ให้เราหลายๆ แบบ แต่ละแบบจะต้องการ Authentication ต่างๆ กันไป ลองเข้าไปดูได้ที่ Instagram API Endpoints ซึ่งถ้าดูจาก Location Endpoints แล้ว เราต้องใช้ Access Token

https://api.instagram.com/v1/locations/search?lat=48.858844&lng=2.294351&access_token=ACCESS-TOKEN

ซึ่งการจะได้ Access Token มา ทำได้ 2 แบบ คือ server-side และ client-side เพื่อความง่าย ในโปรเจคท์นี้ผมจึงใช้ client-side ไปก่อนนะครับ (แต่มันจะมีข้อจำกัดหลายๆ อย่างในวิธีนี้) ซึ่งสร้างได้โดยวิธีต่อไปนี้

  1. เข้าไปแล้วสมัครสมาชิก เลือก Register new Client ID กรอกข้อมูลอะไรไป
  2. ตรง Valid redirect URIs ตั้งเป็น http://localhost ไปก่อน
  3. ไม่ต้องติ๊กช่อง Disable implicit OAuth
  4. ใส่ url ต่อไปนี้ลงไปใน address bar “https://instagram.com/oauth/authorize/?client_id=[CLIENT_ID_HERE]&redirect_uri=http://localhost&response_type=token” โดยเปลี่ยน [CLIENT_ID] ให้เป็น Client ID ที่เขาให้มาในข้อ 1
  5. สังเกตที่ url เราจะได้ access token มาครับ “http://localhost/#access_token=[ACCESS_TOKEN]” เก็บ Token นี้ไว้ใช้ต่อไป

เริ่มกันเลย

1. เตรียม HTML, CSS, JS ไฟล์เบื้องต้น

ข้อนี้ไม่ขอบรรยายเยอะนะครับ ผมใช้ library เป็น bootstrap และ jQuery ส่วน HTML ก็มีโครงสร้างไม่กี่อย่าง ที่เหลือเป็น JavaScript เรียกภาพมา append เข้าไปใน DOM เฉยๆ ครับ ไฟล์มีอยู่สามไฟล์

ig-project-03

index.html

<body>

	<div class="container main">

		<div class="row">
			<div class="col-sm-12 title-div">
				<h1 class="text-center">Instagram API demo</h1>
				<h2 class="text-center">Get image around latitude and longtitude</h2>
			</div>
		</div>

		<div class="row">
			<div class="col-sm-12">
				<ul class="list-inline location-list">
					<li class="location-list-item">Wat Phra Kaew</li>
					<li class="location-list-item">Wat Pho</li>
					<li class="location-list-item">Wat Arun</li>
					<li class="location-list-item">Wat Ratchabophit</li>
					<li class="location-list-item">Wat Ratchanaddaram</li>
					<li class="location-list-item">Wat Ratchapradit</li>
					<li class="location-list-item">Wat Saket and the Golden Mount</li>
					<li class="location-list-item">Wat Suthat and the Giant Swing</li>
					<li class="location-list-item">Wat Thepthidaram</li>
				</ul>
			</div>
		</div>

		<div class="row">
			<div class="col-sm-12 ig" id="ig">
				<h4>Some Instagram pictures in 100 metre around this area</h4>
				<p id="ig-info" class="bg-info">Please select temple to view.</p>
				<div id="image-area"></div>
			</div>
		</div>

	</div>

	<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
	<script src="js/app.js"></script>

</body>

style.css

body {
	background: #bbaa95;
}

.main {
	background: #fff;
}

.title-div {
	margin-bottom: 40px;
}

.location-list {
	text-align: center;
}

.location-list-item {
	margin: 0 10px 10px 0;
	padding: 10px;
	background: #eee;
	cursor: pointer;
	transition: all .1s ease-in;
}

.location-list-item:hover {
	background: #92D8FF;
}

.ig-div {
	display: inline-block;
	width:50%;
}

.ig-div img{
	width:100%;
	padding: 5px;
}

@media only screen and (min-width: 768px) {
	.ig-div {
		display: inline-block;
		width:20%;
	}
}

2. เตรียมข้อมูลวัด

สเตปแรกคือเตรียมข้อมูล latitude และ longtitude ของวัดก่อน อันนี้ต้องไปหาเอาเอง (ยกเว้นจะเรียก Google Maps API ให้หาให้นะครับ แต่ก็จะเพิ่มความซับซ้อน)

app.js

var placeData = [
	{
		name: 'Wat Phra Kaew',
		lat: 13.751399,
		lng: 100.492541,
	}, {
		name: 'Wat Pho',
		lat: 13.746575,
		lng: 100.492728,
	}, {
		name: 'Wat Arun',
		lat: 13.743703,
		lng: 100.489005,
	}, {
		name: 'Wat Ratchabophit',
		lat: 13.749143,
		lng: 100.497345,
	}, {
		name: 'Wat Ratchanaddaram',
		lat: 13.754767,
		lng: 100.504301,
	}, {
		name: 'Wat Ratchapradit',
		lat: 13.749453,
		lng: 100.495848,
	}, {
		name: 'Wat Saket and the Golden Mount',
		lat: 13.753865,
		lng: 100.508134,
	}, {
		name: 'Wat Suthat and the Giant Swing',
		lat: 13.751042,
		lng: 100.501006,
	}, {
		name: 'Wat Thepthidaram',
		lat: 13.753385,
		lng: 100.504221,
	}
];

3. bind Event ให้เรียก function เวลาเราคลิกที่ลิสท์

app.js

$('.location-list-item').click(function() {
	var index = $(this).index();
	var lat = placeData[index].lat;
	var lng = placeData[index].lng;
	getIgPhoto(lat, lng);
});

แต่ละ li จะมี class ‘.location-list-item’ อยู่ ใช้ class นั้นเป็น selector เมื่อคลิกที่ element นั้น

  • ได้ index ของ element ที่เราคลิก จากนั้นนำ index นั้นไปเอาค่า lat, lng ที่เราเตรียมไว้มาจาก placeData array
  • เรียก function ‘getIgPhoto’ โดยใช้ lat, lng ของ placeData นั้นเป็น arguments

4. AJAX call ครั้งแรก

ในการจะทำ project นี้ เราต้องเรียก instagram API 2 ครั้งครับ เมื่อเราใส่ latitude และ longtitude ไป เราจะได้ Array ของ location-id กลับมา จากนั้นจึงนำ location-id นั้นไปเรียกอีกครั้ง จึงจะได้ image object จริงๆ กลับมา

อธิบายหลักการก็คือ instagram เก็บข้อมูลสถานที่ไว้ในฐานข้อมูลชุดหนึ่ง เวลาเราอัพรูปใน instagram แล้วเราเลือกให้แสดง location ด้วย ตัว location ต่างๆ ที่ขึ้นมาให้เลือกนี่แหละครับคือพิกัดที่ instagram เก็บข้อมูลไว้ สำหรับ location search endpoint ก็คือเราให้พิกัด latitude(LAT) longtitude(LNG) ไป แล้วกำหนดระยะทาง(DISTANCE) รอบๆ พิกัดนี้ instagram จะไปดึง location ทั้งหมดในฐานข้อมูล ที่อยู่รอบๆ พิกัดนี้ในระยะที่เรากำหนดมา เช่น รอบพิกัด 1,000 เมตร รอบวัดพระแก้ว อาจจะมี 15 location เป็นต้น

ig-project-04

แสดง location search endpoints

เช่น ในตัวอย่างของ API Docs คือ ถ้าให้พิกัดฟอไอเฟลไป ก็จะได้ response กลับมาแบบนี้

ig-project-05

เพราะฉะนั้น code เราจึงเป็นแบบนี้

	// เตรียมตัวแปลและ select element ที่ต้องใช้รอไว้
	var locationURLList = [],
		imageObjList = [],
		imageList = [],
		infoBox = $('#ig-info'),
		imgDiv = $('.ig-div');

	// เอารูปเดิมออก และบอก user ว่ากำลังโหลดรูปนะ
	imgDiv.remove();
	infoBox.show().removeClass('bg-danger').addClass('bg-info').text("Loading...");

	// Make AJAX call
	       // เพื่อจะได้ array ของ location-id
	$.ajax({
		type: 'GET',
		dataType: 'jsonp',
		cache: true,
		url: 'https://api.instagram.com/v1/locations/search?lat=' 
      + lat + '&lng=' + lng + '&distance=100&access_token=[ใส่ ACCESS TOKEN]'
	}).done(function(data) {
		// ขั้นตอนต่อไป

กลับไปดูที่ API docs กันอีกครั้ง ถ้าเราอยากได้รูปที่อยู่รอบๆ location-id หนึ่งๆ เราต้องเตรียม endpoint format ดังต่อไปนี้

ig-project-06

นำ object ที่ response กลับมา มาสร้าง array ของ url สำหรับการ call ครั้งที่สอง

	}).done(function(data) {

		// เมื่อทุกอย่างเสร็จสิ้น loop ผ่านแต่ละ item ใน array เพื่อสร้างเป็น url 
  // ที่เราต้อง call ในครั้งที่สอง
		for (var i = 0; i < data.data.length; i++) {
		// เตรียม url จาก location_id
			var targetURL = 'https://api.instagram.com/v1/locations/' 
       + data.data[i].id + '/media/recent?access_token=[ใส่ ACCESS TOKEN]';
			locationURLList.push(targetURL);
		}

		// เนื่องจากบางทีรอบๆ พิกัดหนึ่งๆ มีหลาย location มาก จึงเอามาแค่ 10 อัน 
  // แต่ถ้าใครต้องการทั้งหมดสามารถตัดบรรทัดนี้ทิ้งได้ครับ
		locationURLList = locationURLList.slice(0, 10);

	}).done(function(){

		// ขั้นตอนต่อไป

เมื่อเสร็จขั้นตอนนี้ ใน locationURLlist ของเราจะมี utl ใน format นี้ ‘https://api.instagram.com/v1/locations/{location-id}/media/recent?access_token=ACCESS-TOKEN’ จำนวน 10 url


5. AJAX call ครั้งที่สอง

ต้องบอกก่อนว่าเรื่อง promise นี่ผมไม่แม่นจริงๆ นะครับ แฮ่ๆ อธิบายตามที่เข้าใจก็คือ เราต้องการ call AJAX พร้อมๆ กันหลายอันแบบขนาน (parallel) เราจึงใช้ $.when แต่เนื่องจาก $.when รับค่าเป็น deferred object เราจึงต้อง map แต่ละ url ให้ return มาเป็น array ของ deferred object แล้ว apply มาใส่ $.when จากนั้นเมื่อ done แล้วจึงทำขั้นตอนต่อไป ซึ่งไม่มีอะไรซับซ้อนแล้วหลังจากตรงนี้

ลองอ่านเพิ่มเติมได้ที่ link ต่อไปนี้นะครับ

}).done(function(){

		// เรียก AKAX ครั้งที่สอง ด้วยแต่ละ url ใน array
		$.when.apply($, locationURLList.map(function(url) {

			return $.ajax({
				type: "GET",
				dataType: "jsonp",
				cache: true,
				url: url
			});

		})).done(function(){

			 // ซ่อนแถบที่เอาไว้บอกว่า loading...
			 infoBox.hide();
			 // loop ผ่าน object ที่ response กลับมาแล้วเอา image object นั้นไป concat 
    // เข้าไปใน imageObjList ซึ่งเป็น array ของ image object
			 for (var i = 0; i < arguments.length; i++) {
				  imageObjList = imageObjList.concat(arguments[i][0].data);
			 }

			 // ถ้าอยากได้รูปแค่ 10 รูป uncomment บรรทัดนี้
			 // imageObjList = imageObjList.slice(0, 10);

    // เตรียม image div สำหรับใส่รูป 
    var imageContainer = $('<div>');

			 // นำแต่ละ image object มาหาค่าที่เราต้องการ
			 for (var j = 0; j < imageObjList.length; j++) {

    // หาเวลาที่ user สร้างภาพขึ้นมา 
			 var time = new Date(parseInt(imageObjList[j].created_time) * 1000);
			 var fullDate = time.getDate() + '-' + time.getMonth() + '-' 
        + time.getFullYear();
 
    // append เข้าไปใน element ที่เราต้องการ
    imageContainer.append('<div class="ig-div"><a href="' 
      + imageObjList[j].link + '"><img src="' 
      + imageObjList[j].images.low_resolution.url + '" /></a>' 
      + fullDate  +'</div>');
    }

    $('#image-area').html(imageContainer);

    // ไม่ต้องแสดง error message แล้ว
    clearTimeout(instagramRequestTimeout);

    });
});

	 // ถ้ามีปัญหาอะไรใน AJAX call ให้แสดง error message
	 var instagramRequestTimeout = setTimeout(function() {
		  infoBox.removeClass('bg-info').addClass('bg-danger').text("Fail to get instagram resources");
	}, 8000);

ก็หมดแล้วครับ


6. สรุปภาพรวมของ JS ทั้งไฟล์

app.js ก็อปไปแล้วเปลี่ยนตรง [ACCESS_TOKEN] ก็ใช้ได้เลยครับ

var placeData = [
	{
		name: 'Wat Phra Kaew',
		lat: 13.751399,
		lng: 100.492541,
	}, {
		name: 'Wat Pho',
		lat: 13.746575,
		lng: 100.492728,
	}, {
		name: 'Wat Arun',
		lat: 13.743703,
		lng: 100.489005,
	}, {
		name: 'Wat Ratchabophit',
		lat: 13.749143,
		lng: 100.497345,
	}, {
		name: 'Wat Ratchanaddaram',
		lat: 13.754767,
		lng: 100.504301,
	}, {
		name: 'Wat Ratchapradit',
		lat: 13.749453,
		lng: 100.495848,
	}, {
		name: 'Wat Saket and the Golden Mount',
		lat: 13.753865,
		lng: 100.508134,
	}, {
		name: 'Wat Suthat and the Giant Swing',
		lat: 13.751042,
		lng: 100.501006,
	}, {
		name: 'Wat Thepthidaram',
		lat: 13.753385,
		lng: 100.504221,
	}
];

$('.location-list-item').click(function() {
	var index = $(this).index();
	var lat = placeData[index].lat;
	var lng = placeData[index].lng;
	getIgPhoto(lat, lng);
});


function getIgPhoto(lat, lng) {

	// Prepare variable
	var locationURLList = [],
		imageObjList = [],
		imageList = [],
		infoBox = $('#ig-info'),
		imgDiv = $('.ig-div');

	// Remove old image and tell the user we're loading images.
	imgDiv.remove();
	infoBox.show().removeClass('bg-danger').addClass('bg-info').text("Loading...");

	// Make AJAX call
	// The first call will get an array of location ID. We have to use those ID to make url for call again to get real image objects.
	$.ajax({
		type: 'GET',
		dataType: 'jsonp',
		cache: true,
		url: 'https://api.instagram.com/v1/locations/search?lat=' + lat + '&lng=' + lng + '&distance=100&access_token=[ACCESS_TOKEN]'
	}).done(function(data) {

		// If request done, continue the next process
		// loop through data from instagram and make URL for second call.
		for (var i = 0; i < data.data.length; i++) {
			// Create target URL from location ID and push to the URL list array
			var targetURL = 'https://api.instagram.com/v1/locations/' + data.data[i].id + '/media/recent?access_token=[ACCESS_TOKEN]';
			locationURLList.push(targetURL);
		}

		// Just 10 location URL is enough. When make a request to each URL, will get a lot of images.
		locationURLList = locationURLList.slice(0, 10);

	}).done(function(){

		// Make an AJAX call with each URL in array.
		$.when.apply($, locationURLList.map(function(url) {

			return $.ajax({
				type: "GET",
				dataType: "jsonp",
				cache: true,
				url: url
			});

		})).done(function(){

			// If got all data from each URL then hide the info box.
			infoBox.hide();
			// Loop through returned data and make an array of image objects.
			for (var i = 0; i < arguments.length; i++) {
				imageObjList = imageObjList.concat(arguments[i][0].data);
			}

			// We want only first 10 images to display.
			// imageObjList = imageObjList.slice(0, 10);

			var imageContainer = $('<div>');

			// Append images to the page
			for (var j = 0; j < imageObjList.length; j++) {
				var time = new Date(parseInt(imageObjList[j].created_time) * 1000);
				var fullDate = time.getDate() + '-' + time.getMonth() + '-' + time.getFullYear();
				imageContainer.append('<div class="ig-div"><a href="' + imageObjList[j].link + '"><img src="' + imageObjList[j].images.low_resolution.url + '" /></a>' + fullDate  +'</div>');
			}

			$('#image-area').html(imageContainer);

			// Do not display error message
			clearTimeout(instagramRequestTimeout);

		});

	});

	// But if there're any problem in the AJAX call process, tell the user.
	var instagramRequestTimeout = setTimeout(function() {
		infoBox.removeClass('bg-info').addClass('bg-danger').text("Fail to get instagram resources");
	}, 8000);
}

อยากทำอะไรมากไปกว่านี้

เนื่องจากที่ผมทำเป็นแค่โปรเจคท์คร่าวๆ เฉยๆครับ ถ้าอยากทำมากกว่านี้มีไอเดียหลายอย่างที่ทำได้


Search bar

เราไม่ต้องเตรียมข้อมูลพิกัดไว้ก่อน แต่เราสามารถให้ user ป้อน address ที่ต้องการแล้วเราใช้ Google Maps Geocoding API เปลี่ยนเป็น latitude longtitude ได้ ลองดูเพิ่มเติมได้ครับ

ig-project-07

สุดท้ายจะออกมาเหมือนเว็บนี้ Worldcam


Cache

เนื่องจากตอนนี้ทุกครั้งที่เราคลิก เว็บจะต้องเรียก instagram API ใหม่ทุกครั้ง ซึ่งมันทำให้ช้า และตัวเว็บก็ไม่ได้ต้องการความ real-time ขนาดนั้น ดังนั้นถ้าเรา cache รูปไว้ซักระยะหนึ่งก่อน เช่น load แค่ครั้งแรกครั้งเดียว ก็คงจะเร็วขึ้นครับ


Sort images by time

เนื่องจากเราเรียก AJAX ตาม location-id เพราะฉะนั้น response ที่กลับมาจึงเรียงตาม location-id แต่ response ที่กลับมาก็มี created_time เป็น property อยู่แล้ว ดังนั้นก่อนจะ  append เข้าไปใน DOM เราสามารถ sort object ใน array ได้ ลองดู Array.prototype.sort() ครับ


Display image in Google Maps

เนื่องจากแต่ละ location-id มันก็มี latitude, longitude ของตัวเองอยู่แล้ว จริงๆ ก็เอามา pin ใน google maps ได้

และอื่นๆ อีกมากมายครับ


ปิดท้าย

คือถึงจะเขียนมาจนจบแล้ว ผมก็ยังนึก use case ของ project นี้จริงๆ ไม่ค่อยออกครับ ออกแนว gimmick มากกว่า เพราะ ที่ใช้บ่อยจริงๆ น่าจะเป็นดึง instagram image ตาม hashtag มากกว่า งาน event ใช้กันพรึ่บแน่นอน 55 แต่ก็ไม่เป็นไรครับ ถือว่าฝึกฝีมือ หวังว่าจะเป็นประโยชน์นะครับ 🙂

You Might Also Like

No Comments

Leave a Reply