First you need to install npm and then install required node modules:
npm install jsdomnpm install fsnpm install underscore
to run the script, run node index.js. It will output the output.html file, which can be viewed in a browser.
| <html> | |
| <body> | |
| <head> | |
| <style> | |
| html, body { | |
| font-family: sans-serif; | |
| } | |
| .location-list-header { | |
| margin-top: 50px; | |
| font-size: 30px; | |
| text-align: center; | |
| text-transform: uppercase; | |
| letter-spacing: .2rem; | |
| color: #777777; | |
| } | |
| .location-lists { | |
| text-align: center; | |
| } | |
| .criteria-name { | |
| text-transform: uppercase; | |
| color: #333333; | |
| letter-spacing: .15rem; | |
| } | |
| .location-list { | |
| list-style: none; | |
| text-align: center; | |
| color: #555555; | |
| } | |
| </style> | |
| </head> | |
| <h1 class="location-list-header">Instarate</h1> | |
| <div class="location-lists"></div> | |
| </body> | |
| </html> |
| // In order to run the script, the following node modules | |
| // must be installed | |
| var jsdom = require('jsdom'); | |
| var fs = require('fs'); | |
| var _ = require('underscore'); | |
| // Create a JS DOM so that we can easily make AJAX calls and | |
| // build up an HTML file using jQuery | |
| jsdom.env({ | |
| html: fs.readFileSync('./index.html'), | |
| // pull in jQuery from their CDN | |
| scripts: ['http://code.jquery.com/jquery.js'], | |
| done: function (errors, window) { | |
| var $ = window.$; | |
| var instarate = { | |
| // Instagram API client ID | |
| _clientId: '77e63572590f425cbf5005e410100617', | |
| // Instagram API base endpoint | |
| _apiEndpoint: 'https://api.instagram.com/v1', | |
| // Intagram API media search endpoint | |
| _mediaSearchEndpoint: '/media/search', | |
| // Stores location coordinates of locations to compare in Instarate | |
| // Provide as list of lat / lng coords with specified decimal precision | |
| // Can provide any number of coordinate pairs per location since Instagram | |
| // API can only search within 5km radius of each point | |
| locationCoordinates: { | |
| sanFrancisco: [ | |
| { lat: '37.765992', lng: '-122.479864' }, | |
| { lat: '37.778405', lng: '-122.423648' } | |
| ], | |
| seattle: [ | |
| { lat: '47.617238', lng: '-122.319911' }, | |
| { lat: '47.652527', lng: '-122.306823' } | |
| ] | |
| // etc | |
| }, | |
| // Stores search criteria keywords that are used for ranking the locations | |
| // Each key is a metric, and value is a list of tags in media objects | |
| // that should count the post for the metric | |
| searchCriteriaKeywords: { | |
| outdoorsy: [ | |
| 'hike', | |
| 'hiking', | |
| 'nature', | |
| 'outdoors' | |
| ], | |
| dogFriendly: [ | |
| 'pooch', | |
| 'woof', | |
| 'dog', | |
| 'dogs' | |
| ] | |
| }, | |
| locationRankings: {}, | |
| rawMediaObjects: {}, | |
| filteredMediaObjects: {}, | |
| oldestMediaObjectTimestamps: { | |
| sanFrancisco: null, | |
| seattle: null | |
| }, | |
| // GET request to Instagram media search endpoint for specified lat / lng | |
| // Save results to this.rawMediaObjects | |
| getMediaObjectsForLatLng: function (args) { | |
| var url = this._apiEndpoint + this._mediaSearchEndpoint; | |
| var callback = args.callback; | |
| var location = args.location; | |
| var params = { | |
| lat: args.lat, | |
| lng: args.lng, | |
| distance: 5000, | |
| max_timestamp: args.maxTimestamp, | |
| client_id: this._clientId | |
| }; | |
| $.ajax({ | |
| url: url, | |
| data: params, | |
| success: function (data) { | |
| if (data.meta && data.meta.code === 200) { | |
| data.data.forEach( function (mediaObject) { | |
| // let's only save media objects with tags | |
| if (mediaObject.tags.length > 0) { | |
| if (this.rawMediaObjects[location]) { | |
| this.rawMediaObjects[location].push(mediaObject); | |
| } else { | |
| this.rawMediaObjects[location] = [mediaObject]; | |
| } | |
| if (mediaObject.created_time < this.oldestMediaObjectTimestamps[location]) { | |
| this.oldestMediaObjectTimestamps[location] = mediaObject.created_time; | |
| } | |
| } | |
| }.bind(this)); | |
| callback(); | |
| } | |
| }.bind(this), | |
| error: function (data) { | |
| console.log('*** Error response from Instagram API ' + url + ' ***'); | |
| console.log(data); | |
| } | |
| }); | |
| }, | |
| // Kick off a callback chain of methods to retrieve media objects | |
| // for the lat / lng coordinates specified in this.locationCoordinates | |
| // for location | |
| getMediaObjectsForLocation: function (location, outerCallback) { | |
| var callChain = [outerCallback]; | |
| var locationList = this.locationCoordinates[location]; | |
| var maxTimestamp = this.oldestMediaObjectTimestamps[location]; | |
| for (var i = 0; i < locationList.length; i++) { | |
| var callback = callChain[0] || function () {}; | |
| var coords = locationList[i]; | |
| var args = { | |
| location: location, | |
| lat: coords.lat, | |
| lng: coords.lng, | |
| maxTimestamp: maxTimestamp, | |
| callback: callback | |
| } | |
| callChain.unshift(this.getMediaObjectsForLatLng.bind(this, args)); | |
| } | |
| callChain[0](); | |
| }, | |
| // Kick off a callback chain of methods to retrieve media objects | |
| // for all locations specified in this.locationCoordinates | |
| getMediaObjectsForAllLocations: function (outerCallback) { | |
| var callChain = [outerCallback]; | |
| Object.keys(this.locationCoordinates).forEach( function (location) { | |
| var callback = callChain[0] || function () {}; | |
| callChain.unshift(this.getMediaObjectsForLocation.bind(this, location, callback)); | |
| }.bind(this)); | |
| callChain[0](); | |
| }, | |
| // Organize this.rawMediaObjects in to filtered sets | |
| // We create keys on this.filteredMediaObjects for each city | |
| // With sub-keys for the specified search criteria keywords | |
| filterMediaObjectsBySearchCriteria: function () { | |
| Object.keys(this.searchCriteriaKeywords).forEach ( function (criteria) { | |
| var keywordList = this.searchCriteriaKeywords[criteria]; | |
| Object.keys(this.rawMediaObjects).forEach( function (location) { | |
| this.rawMediaObjects[location].forEach( function (mediaObject) { | |
| // should all be lowercase | |
| if (_.intersection(keywordList, mediaObject.tags).length > 0) { | |
| var locationResults = this.filteredMediaObjects[location]; | |
| if (locationResults) { | |
| if (locationResults[criteria]) { | |
| locationResults[criteria].push(mediaObject); | |
| } else { | |
| locationResults[criteria] = [mediaObject]; | |
| } | |
| } else { | |
| locationResults = {}; | |
| locationResults[criteria] = [mediaObject]; | |
| this.filteredMediaObjects[location] = locationResults; | |
| } | |
| } | |
| }.bind(this)); | |
| }.bind(this)); | |
| }.bind(this)); | |
| }, | |
| // Sort locations into ranked lists in this.locationRankings | |
| // Each key is a criteria and the list is the order in which each location | |
| // most represents the criteria | |
| rankLocations: function () { | |
| Object.keys(this.searchCriteriaKeywords).forEach( function (criteria) { | |
| var criteriaRanking = []; | |
| Object.keys(this.filteredMediaObjects).forEach( function (location) { | |
| var filteredMediaObjectsForLocation = this.filteredMediaObjects[location]; | |
| var count = 0; | |
| if (filteredMediaObjectsForLocation && filteredMediaObjectsForLocation[criteria]) { | |
| count = this.filteredMediaObjects[location][criteria].length; | |
| } | |
| criteriaRanking.push([location, count]); | |
| }.bind(this)); | |
| this.locationRankings[criteria] = _.sortBy(criteriaRanking, function (locPair) { | |
| return -1 * locPair[1]; // sort desc | |
| }); | |
| }.bind(this)); | |
| }, | |
| // Write the ranked locations to the DOM | |
| writeLocationRankingsToDom: function (callback) { | |
| var $locationLists = $('.location-lists'); | |
| Object.keys(this.locationRankings).forEach( function (criteria) { | |
| $locationLists.append($('<h2>').addClass('criteria-name').html(criteria)); | |
| $locationLists.append($('<ol>').addClass('location-list') | |
| .addClass('location-list-' + criteria)); | |
| this.locationRankings[criteria].forEach( function (location) { | |
| $('.location-list-' + criteria).append($('<li>').html(location[0])); | |
| }); | |
| }.bind(this)); | |
| if (callback) callback(); | |
| }, | |
| // for testing | |
| writeFilteredMediaObjectsToDom: function (callback) { | |
| var $list = $('.location-lists'); | |
| Object.keys(this.filteredMediaObjects).forEach( function (location) { | |
| $list.append($('<div>').addClass('location-name').html(location)); | |
| var filteredMediaObjectsForLocation = this.filteredMediaObjects[location]; | |
| Object.keys(filteredMediaObjectsForLocation).forEach( function (criteria) { | |
| var criteriaMediaObjects = filteredMediaObjectsForLocation[criteria]; | |
| criteriaMediaObjects.forEach( function (mediaObject) { | |
| var date = new Date(mediaObject.created_time * 1000); | |
| var tags = mediaObject.tags.join(); | |
| var $li = $('<div>').html('' + date + ': ' + tags); | |
| $list.append($li); | |
| }); | |
| }); | |
| }.bind(this)); | |
| if (callback) callback(); | |
| }, | |
| // for testing | |
| writeRawMediaObjectsToDom: function (callback) { | |
| var $list = $('.location-lists'); | |
| Object.keys(this.rawMediaObjects).forEach( function (location) { | |
| $list.append($('<div>').addClass('location-name').html(location)); | |
| this.rawMediaObjects[location].forEach( function (mediaObject) { | |
| var date = new Date(mediaObject.created_time * 1000); | |
| var tags = mediaObject.tags.join(); | |
| var $li = $('<div>').html('' + date + ': ' + tags); | |
| $list.append($li); | |
| }); | |
| }.bind(this)); | |
| if (callback) callback(); | |
| }, | |
| // Write the contents of the 'DOM' to output.html | |
| writeFileContents: function () { | |
| var newContents = '<html>' + $('html').html() + '</html>'; | |
| fs.writeFileSync('output.html', newContents, {}, function (error) { | |
| if (error) throw error; | |
| }); | |
| }, | |
| // Intended to be used as a callback once data has been retrieved from API | |
| // Run the sequence of processing required to output lists | |
| processData: function () { | |
| this.filterMediaObjectsBySearchCriteria(); | |
| this.rankLocations(); | |
| //this.writeRawMediaObjectsToDom(this.writeFileContents); | |
| //this.writeFilteredMediaObjectsToDom(this.writeFileContents); | |
| this.writeLocationRankingsToDom(this.writeFileContents); | |
| } | |
| }; | |
| // kick off the logic | |
| var callback = instarate.processData.bind(instarate); | |
| instarate.getMediaObjectsForAllLocations(callback); | |
| } | |
| }); |