Permalink
Cannot retrieve contributors at this time
Fetching contributors…
| 'use strict'; | |
| var mediaFinder = require('./mediafinder.js'); | |
| var jsdom = require('jsdom').jsdom; | |
| var Canvas = require('canvas'); | |
| var Image = Canvas.Image; | |
| var request = require('request'); | |
| var Histogram = require('./histogram.js'); | |
| var Twitter = require('node-twitter'); | |
| var env = require('node-env-file'); | |
| if (require('fs').existsSync(__dirname + '/.env')) { | |
| env(__dirname + '/.env'); | |
| } | |
| var TWEET_BREAKING_NEWS_CANDIDATES = false; | |
| var DUMP_MEDIA_GALLERIES = false; | |
| var twitterRestClient = new Twitter.RestClient( | |
| process.env.MEDIA_GALLERY_API_KEY, | |
| process.env.MEDIA_GALLERY_API_SECRET, | |
| process.env.MEDIA_GALLERY_ACCESS_TOKEN, | |
| process.env.MEDIA_GALLERY_ACCESS_TOKEN_SECRET | |
| ); | |
| var recentTweetsBuffer = []; | |
| var document = jsdom(); | |
| var illustrator = { | |
| bwTolerance: 1, | |
| cols: 10, | |
| rows: 10, | |
| minAge: 0, | |
| maxAge: 1 * 24 * 60 * 60 * 1000, // 1 day | |
| threshold: 15, | |
| similarTiles: 67, | |
| considerFaces: false, | |
| weights: { | |
| likes: 2, | |
| shares: 4, | |
| comments: 8, | |
| views: 1, | |
| crossNetwork: 32, | |
| recency: 2 | |
| }, | |
| mediaGallerySize: 25, | |
| mediaGalleryWidth: 999, | |
| mediaGalleryMargin: 4, | |
| mediaGalleryFontSize: 8, | |
| mediaItemHeight: 166, | |
| canvas: null, | |
| ctx: null, | |
| faviconCache: {}, | |
| searchMediaItems: function(searchTerms, wikipediaUrl, callback) { | |
| var mediaItems = {}; | |
| var clusters = []; | |
| illustrator.canvas = new Canvas(100, 100); | |
| illustrator.ctx = illustrator.canvas.getContext('2d'); | |
| illustrator.ctx.patternQuality = 'best'; | |
| illustrator.ctx.filter = 'best'; | |
| illustrator.ctx.antialias = 'subpixel'; | |
| var returnSearchResults = function(searchResultsDelivered) { | |
| for (var term in searchResultsDelivered) { | |
| // not all search requests finished yet | |
| if (searchResultsDelivered[term] === false) { | |
| return false; | |
| } | |
| } | |
| // all search requests finished | |
| // used to only insert unique media items | |
| var containsMediaItem = function(source, micropostUrl) { | |
| for (var i = 0, len = source.length; i < len; i++) { | |
| if (source[i].micropostUrl === micropostUrl) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| }; | |
| // populate the final results object | |
| var result = {}; | |
| for (var term in searchResultsDelivered) { | |
| for (var network in searchResultsDelivered[term]) { | |
| var subResults = searchResultsDelivered[term]; | |
| if (!result[network]) { | |
| result[network] = []; | |
| } | |
| if (subResults[network]) { | |
| for (var i = 0, len = subResults[network].length; i < len; i++) { | |
| var mediaItem = subResults[network][i]; | |
| if (!containsMediaItem(result[network], mediaItem.micropostUrl)) { | |
| result[network].push(mediaItem); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| illustrator.retrieveMediaItems(result, mediaItems, clusters, searchTerms, | |
| wikipediaUrl, callback); | |
| }; | |
| var searchResultsDelivered = {}; | |
| /* jshint maxlen:false */ | |
| var userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.116 Safari/537.36'; | |
| /* jshint maxlen:80 */ | |
| for (var term in searchTerms) { | |
| // needed to see if all social networks have returned results | |
| searchResultsDelivered[term] = false; | |
| (function(current) { | |
| mediaFinder.search('combined', current, userAgent, function(result) { | |
| var unquotedTerm = current.replace(/^"/, '').replace(/"$/, ''); | |
| searchResultsDelivered[unquotedTerm] = result; | |
| returnSearchResults(searchResultsDelivered); | |
| }); | |
| })('"' + term + '"'); | |
| } | |
| }, | |
| retrieveMediaItems: function(results, mediaItems, clusters, searchTerms, | |
| wikipediaUrl, callback) { | |
| var checkMediaItemStatuses = function(target) { | |
| for (var key in mediaItems) { | |
| if (mediaItems[key].status !== target) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| }; | |
| var preloadImage = function(src, success, error) { | |
| request.get({url: src, encoding: null}, function(err, response, body) { | |
| if (err || response.statusCode !== 200) { | |
| return error(src); | |
| } | |
| var image = new Image(); | |
| try { | |
| image.src = new Buffer(body, 'binary'); | |
| return success(image); | |
| } catch(e) { | |
| return error(src); | |
| } | |
| }); | |
| }; | |
| var preloadFullImage = function(posterUrl) { | |
| // for photos, load the media url as full image | |
| // for videos, load the (already cached) thumbnail as full image | |
| var mediaUrl = posterUrl; | |
| if (mediaItems[posterUrl].type === 'photo') { | |
| mediaUrl = mediaItems[posterUrl].mediaUrl; | |
| } | |
| preloadImage( | |
| mediaUrl, | |
| function(image) { | |
| successFullImage(image, posterUrl); | |
| }, | |
| function() { | |
| errorFullImage(posterUrl); | |
| }); | |
| }; | |
| var successThumbnail = function(image, posterUrl) { | |
| if (!illustrator.calculateHistograms(image, posterUrl, mediaItems)) { | |
| errorThumbnail(posterUrl); | |
| } else { | |
| preloadFullImage(posterUrl); | |
| } | |
| }; | |
| var errorThumbnail = function(posterUrl) { | |
| delete mediaItems[posterUrl]; | |
| if (checkMediaItemStatuses('loaded')) { | |
| illustrator.calculateDistances(mediaItems, clusters, searchTerms, | |
| wikipediaUrl, callback); | |
| } | |
| }; | |
| var successFullImage = function(image, posterUrl) { | |
| mediaItems[posterUrl].status = 'loaded'; | |
| mediaItems[posterUrl].fullImage = { | |
| width: image.width, | |
| height: image.height, | |
| image: image | |
| }; | |
| if (checkMediaItemStatuses('loaded')) { | |
| illustrator.calculateDistances(mediaItems, clusters, searchTerms, | |
| wikipediaUrl, callback); | |
| } | |
| }; | |
| var errorFullImage = function(posterUrl) { | |
| delete mediaItems[posterUrl]; | |
| if (checkMediaItemStatuses('loaded')) { | |
| illustrator.calculateDistances(mediaItems, clusters, searchTerms, | |
| wikipediaUrl, callback); | |
| } | |
| }; | |
| var numResults = 0; | |
| for (var service in results) { | |
| results[service].forEach(function(item) { | |
| numResults++; | |
| item.origin = service; | |
| item.status = false; | |
| item.considerMediaItem = true; | |
| mediaItems[item.posterUrl] = item; | |
| var posterUrl = item.posterUrl; | |
| // load the poster url as thumbnail | |
| preloadImage( | |
| posterUrl, | |
| function(image) { | |
| successThumbnail(image, posterUrl); | |
| }, | |
| errorThumbnail); | |
| }); | |
| } | |
| if (numResults === 0) { | |
| return callback(false); | |
| } | |
| }, | |
| calculateHistograms: function(img, posterUrl, mediaItems) { | |
| var canvasWidth = illustrator.canvas.width; | |
| var canvasHeight = illustrator.canvas.height; | |
| // clear the canvas | |
| illustrator.ctx.clearRect (0, 0, canvasWidth, canvasHeight); | |
| // draw the image on the canvas | |
| try { | |
| illustrator.ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight); | |
| } catch(e) { | |
| return false; | |
| } | |
| // calculate the histograms tile-wise | |
| var dw = ~~(canvasWidth / illustrator.cols); | |
| var dh = ~~(canvasHeight / illustrator.rows); | |
| mediaItems[posterUrl].tileHistograms = {}; | |
| var len = illustrator.cols * illustrator.rows; | |
| for (var i = 0; i < len; i++) { | |
| // calculate the boundaries for the current tile from the | |
| // image and translate it to boundaries on the main canvas | |
| var mod = (i % illustrator.cols); | |
| var div = ~~(i / illustrator.cols); | |
| var dx = mod * dw; | |
| var dy = div * dh; | |
| // calculate the histogram of the current tile | |
| var histogram = | |
| Histogram.getHistogram(illustrator.ctx, dx, dy, dw, dh, false); | |
| mediaItems[posterUrl].tileHistograms[i] = { | |
| r: histogram.pixel.r, | |
| g: histogram.pixel.g, | |
| b: histogram.pixel.b | |
| }; | |
| } | |
| img = null; | |
| return true; | |
| }, | |
| calculateDistances: function(mediaItems, clusters, searchTerms, wikipediaUrl, | |
| callback) { | |
| var keys = Object.keys(mediaItems); | |
| var len = keys.length; | |
| if (!len) { | |
| return callback(false); | |
| } | |
| var abs = Math.abs; | |
| // reset distances | |
| for (var i = 0; i < len; i++) { | |
| mediaItems[keys[i]].distances = {}; | |
| } | |
| // commonly applied luminance factors | |
| var rFactor = 0.3; | |
| var gFactor = 0.59; | |
| var bFactor = 0.11; | |
| // black-and-white tolerance | |
| var blackTolerance = illustrator.bwTolerance; | |
| var whiteTolerance = 255 - illustrator.bwTolerance; | |
| for (var i = 0; i < len; i++) { | |
| var outer = keys[i]; | |
| mediaItems[outer].distances = {}; | |
| var outerHisto = mediaItems[outer].tileHistograms; | |
| for (var j = 0; j < len; j++) { | |
| if (j === i) { | |
| continue; | |
| } | |
| var inner = keys[j]; | |
| var innerHisto = mediaItems[inner].tileHistograms; | |
| mediaItems[outer].distances[inner] = {}; | |
| // recycle because of symmetry of distances: | |
| // dist(A<=>B) = dist(B<=>A) | |
| if ((mediaItems[inner].distances) && | |
| (mediaItems[inner].distances[outer])) { | |
| mediaItems[outer].distances[inner] = | |
| mediaItems[inner].distances[outer]; | |
| // calculate new | |
| } else { | |
| for (var k in innerHisto) { | |
| var innerR = innerHisto[k].r; | |
| var innerG = innerHisto[k].g; | |
| var innerB = innerHisto[k].b; | |
| var outerR = outerHisto[k].r; | |
| var outerG = outerHisto[k].g; | |
| var outerB = outerHisto[k].b; | |
| if ((innerR >= blackTolerance && | |
| innerG >= blackTolerance && | |
| innerB >= blackTolerance) && | |
| (outerR >= blackTolerance && | |
| outerG >= blackTolerance && | |
| outerB >= blackTolerance) && | |
| (innerR <= whiteTolerance && | |
| innerG <= whiteTolerance && | |
| innerB <= whiteTolerance) && | |
| (outerR <= whiteTolerance && | |
| outerG <= whiteTolerance && | |
| outerB <= whiteTolerance)) { | |
| mediaItems[outer].distances[inner][k] = | |
| ~~((abs(rFactor * (innerR - outerR)) + | |
| abs(gFactor * (innerG - outerG)) + | |
| abs(bFactor * (innerB - outerB))) / 3); | |
| } else { | |
| mediaItems[outer].distances[inner][k] = null; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| illustrator.filterForMinMaxAgeAndVisibility(mediaItems, clusters, | |
| searchTerms, wikipediaUrl, callback); | |
| }, | |
| filterForMinMaxAgeAndVisibility: function(mediaItems, clusters, searchTerms, | |
| wikipediaUrl, callback) { | |
| var now = Date.now(); | |
| for (var key in mediaItems) { | |
| var mediaItem = mediaItems[key]; | |
| if (mediaItem.timestamp > now) { | |
| mediaItem.considerMediaItem = true; // was: false | |
| } | |
| // perfect | |
| else if ((now - mediaItem.timestamp <= illustrator.maxAge) && | |
| (now - mediaItem.timestamp >= illustrator.minAge)) { | |
| mediaItem.considerMediaItem = true; | |
| // too old or too young | |
| } else { | |
| mediaItem.considerMediaItem = false; | |
| } | |
| } | |
| illustrator.clusterMediaItems(mediaItems, clusters, searchTerms, | |
| wikipediaUrl, callback); | |
| }, | |
| clusterMediaItems: function(mediaItems, clusters, searchTerms, wikipediaUrl, | |
| callback) { | |
| var calculateMinimumSimilarTiles = function() { | |
| return Math.ceil(illustrator.rows * illustrator.cols / 3); | |
| }; | |
| // filter to only consider the, well, considered media items | |
| var keys = Object.keys(mediaItems).filter(function(key) { | |
| return mediaItems[key].considerMediaItem; | |
| }); | |
| var len = keys.length; | |
| var minimumSimilarTiles = calculateMinimumSimilarTiles(); | |
| // the actual clustering | |
| for (var i = 0; i < len; i++) { | |
| if (!keys[i]) { | |
| continue; | |
| } | |
| var outer = keys[i]; | |
| keys[i] = false; | |
| var distanceToOuter = {}; | |
| for (var j = 0; j < len; j++) { | |
| if (j === i) { | |
| continue; | |
| } | |
| var inner = keys[j]; | |
| var similarTiles = 0; | |
| var nulls = 0; | |
| var distance = mediaItems[outer].distances[inner]; | |
| for (var k in distance) { | |
| if (distance[k] !== null) { | |
| if (distance[k] <= illustrator.threshold) { | |
| similarTiles++; | |
| } | |
| } else { | |
| nulls++; | |
| } | |
| } | |
| var minimumRequired; | |
| var similarTilesWithoutNulls = illustrator.similarTiles - nulls; | |
| if (similarTilesWithoutNulls >= minimumSimilarTiles) { | |
| minimumRequired = similarTilesWithoutNulls; | |
| } else { | |
| minimumRequired = minimumSimilarTiles; | |
| } | |
| if (similarTiles >= minimumRequired) { | |
| if (!distanceToOuter[similarTiles]) { | |
| distanceToOuter[similarTiles] = [j]; | |
| } else { | |
| distanceToOuter[similarTiles].push(j); | |
| } | |
| } | |
| } | |
| var members = []; | |
| Object.keys(distanceToOuter).sort(function(a, b) { | |
| return b - a; | |
| }).forEach(function(numSimilarTiles) { | |
| distanceToOuter[numSimilarTiles].forEach(function(key) { | |
| members.push(keys[key]); | |
| keys[key] = false; | |
| }); | |
| }); | |
| clusters.push({ | |
| identifier: outer, | |
| members: members | |
| }); | |
| } | |
| if (clusters.length === 0) { | |
| return callback(false); | |
| } | |
| illustrator.mergeClusterData(mediaItems, clusters, searchTerms, | |
| wikipediaUrl, callback); | |
| }, | |
| mergeClusterData: function(mediaItems, clusters, searchTerms, wikipediaUrl, | |
| callback) { | |
| var calculateDimensions = function(mediaItem) { | |
| // always prefer video over photo, so set the dimensions of videos | |
| // to Infinity, which overrules even high-res photos | |
| return (mediaItem.type === 'video' ? | |
| Infinity : | |
| mediaItem.fullImage.width * mediaItem.fullImage.height); | |
| }; | |
| clusters.forEach(function(cluster) { | |
| var mediaItem = mediaItems[cluster.identifier]; | |
| var socialInteractions = mediaItem.socialInteractions; | |
| var likes = socialInteractions.likes; | |
| var shares = socialInteractions.shares; | |
| var comments = socialInteractions.comments; | |
| var views = socialInteractions.views; | |
| cluster.statistics = { | |
| likes: likes, | |
| shares: shares, | |
| comments: comments, | |
| views: views | |
| }; | |
| cluster.timestamp = mediaItem.timestamp; | |
| var dimensions = calculateDimensions(mediaItem); | |
| cluster.members.forEach(function(url, i) { | |
| var member = mediaItems[url]; | |
| var memberSocialInteractions = member.socialInteractions; | |
| likes += memberSocialInteractions.likes; | |
| shares += memberSocialInteractions.shares; | |
| comments += memberSocialInteractions.comments; | |
| views += memberSocialInteractions.views; | |
| var newDimensions = calculateDimensions(member); | |
| // we have a new cluster identifier | |
| if (newDimensions >= dimensions) { | |
| dimensions = newDimensions; | |
| var oldIdentifier = cluster.identifier; | |
| cluster.identifier = url; | |
| cluster.members[i] = oldIdentifier; | |
| } | |
| // always use the youngest cluster member's timestamp | |
| if (member.timestamp < cluster.timestamp) { | |
| cluster.timestamp = member.timestamp; | |
| } | |
| }); | |
| cluster.statistics = { | |
| likes: likes, | |
| shares: shares, | |
| comments: comments, | |
| views: views | |
| }; | |
| }); | |
| illustrator.rankClusters(mediaItems, clusters, searchTerms, wikipediaUrl, | |
| callback); | |
| }, | |
| rankingFormulas: { | |
| popularity: { | |
| name: 'Popularity', | |
| func: function(a, b) { | |
| var now = Date.now(); | |
| var getAgeFactor = function(timestamp) { | |
| /* 86400000 = 24 * 60 * 60 * 1000 */ | |
| var ageInDays = Math.floor((now - timestamp) / 86400000); | |
| if (ageInDays <= 1) { | |
| return 8; | |
| } else if (ageInDays <= 2) { | |
| return 4; | |
| } else if (ageInDays <= 3) { | |
| return 2; | |
| } else { | |
| return 1; | |
| } | |
| }; | |
| var weights = illustrator.weights; | |
| var combinedStatsA = | |
| weights.likes * a.statistics.likes + | |
| weights.shares * a.statistics.shares + | |
| weights.comments * a.statistics.comments + | |
| weights.views * a.statistics.views + | |
| weights.crossNetwork * a.members.length + | |
| weights.recency * getAgeFactor(a.timestamp); | |
| var combinedStatsB = | |
| weights.likes * b.statistics.likes + | |
| weights.shares * b.statistics.shares + | |
| weights.comments * b.statistics.comments + | |
| weights.views * b.statistics.views + | |
| weights.crossNetwork * b.members.length + | |
| weights.recency * getAgeFactor(b.timestamp); | |
| return combinedStatsB - combinedStatsA; | |
| } | |
| } | |
| }, | |
| rankClusters: function(mediaItems, clusters, searchTerms, wikipediaUrl, | |
| callback) { | |
| clusters.sort(illustrator.rankingFormulas.popularity.func); | |
| illustrator.createMediaGallery(mediaItems, clusters, 'strictOrder', | |
| searchTerms, wikipediaUrl, callback); | |
| /* | |
| illustrator.createMediaGallery(mediaItems, clusters, 'looseOrder', | |
| searchTerms, wikipediaUrl, callback); | |
| */ | |
| }, | |
| createMediaGallery: function(mediaItems, clusters, algorithm, searchTerms, | |
| wikipediaUrl, callback) { | |
| var selectedMediaItems = []; | |
| clusters.forEach(function(cluster, counter) { | |
| if (counter >= illustrator.mediaGallerySize) { | |
| return; | |
| } | |
| var mediaItem = mediaItems[cluster.identifier]; | |
| selectedMediaItems.push(mediaItem); | |
| }); | |
| clusters = null; | |
| illustrator.mediaGalleryAlgorithms[algorithm] | |
| .func(selectedMediaItems, mediaItems, algorithm, searchTerms, | |
| wikipediaUrl, callback); | |
| }, | |
| mediaGalleryAlgorithms: { | |
| strictOrder: { | |
| name: 'Strict order, equal size', | |
| func: function(selectedMediaItems, mediaItems, algorithm, searchTerms, | |
| wikipediaUrl, callback) { | |
| if (selectedMediaItems.length === 0) { | |
| return callback(false); | |
| } | |
| // media gallery algorithm credits to | |
| // http://blog.vjeux.com/2012/image/- | |
| // image-layout-algorithm-google-plus.html | |
| var heights = []; | |
| var margin = illustrator.mediaGalleryMargin; | |
| var size = Math.round(illustrator.mediaGalleryWidth * 2/3); | |
| var calculateSizes = function(images) { | |
| var n = 0; | |
| /* jshint indent:false */ | |
| w: while (images.length > 0) { | |
| /* jshint indent:2 */ | |
| var slice; | |
| var h; | |
| for (var i = 1; i < images.length + 1; ++i) { | |
| slice = images.slice(0, i); | |
| h = getHeight(slice, size); | |
| if (h < illustrator.mediaItemHeight) { | |
| setHeight(slice, h); | |
| n++; | |
| images = images.slice(i); | |
| continue w; | |
| } | |
| } | |
| setHeight(slice, Math.min(illustrator.mediaItemHeight, h)); | |
| n++; | |
| break; | |
| } | |
| }; | |
| var getHeight = function(images, width) { | |
| width -= images.length * 4; | |
| var h = 0; | |
| for (var i = 0, len = images.length; i < len; ++i) { | |
| h += (images[i].getAttribute('data-width') / | |
| images[i].getAttribute('data-height')); | |
| } | |
| return (width / h); | |
| }; | |
| var setHeight = function(images, height) { | |
| heights.push(height); | |
| for (var i = 0, len = images.length; i < len; ++i) { | |
| var width = (height * images[i].getAttribute('data-width') / | |
| images[i].getAttribute('data-height')); | |
| images[i].style.width = Math.round(width) + 'px'; | |
| images[i].style.height = Math.round(height) + 'px'; | |
| images[i].firstChild.firstChild.style.width = | |
| Math.round(width) + 'px'; | |
| images[i].firstChild.firstChild.style.height = | |
| Math.round(height) + 'px'; | |
| } | |
| }; | |
| var fragment = document.createDocumentFragment(); | |
| var divs = []; | |
| selectedMediaItems.forEach(function(item) { | |
| var div = document.createElement('div'); | |
| fragment.appendChild(div); | |
| div.setAttribute('class', 'mediaItem photoBorder'); | |
| div.setAttribute('data-posterurl', item.posterUrl); | |
| div.setAttribute('data-faviconurl', | |
| item.origin.toLowerCase() + '.png'); | |
| var anchor = document.createElement('a'); | |
| anchor.href = item.micropostUrl; | |
| anchor.setAttribute('target', '_newtab'); | |
| var img = document.createElement('img'); | |
| if (item.type === 'photo') { | |
| img.src = item.mediaUrl; | |
| } else { | |
| img.src = item.posterUrl; | |
| } | |
| div.setAttribute('data-width', item.fullImage.width); | |
| div.setAttribute('data-height', item.fullImage.height); | |
| div.setAttribute( | |
| 'style', | |
| 'position:relative !important; float:left !important;'); | |
| anchor.appendChild(img); | |
| div.appendChild(anchor); | |
| var favicon = document.createElement('img'); | |
| favicon.setAttribute('class', 'favicon'); | |
| favicon.src = item.origin.toLowerCase() + '.png'; | |
| div.appendChild(favicon); | |
| divs.push(div); | |
| }); | |
| var mediaGallery = document.createElement('div'); | |
| var container = document.createElement('div'); | |
| mediaGallery.appendChild(fragment); | |
| container.appendChild(mediaGallery); | |
| calculateSizes(divs); | |
| var height = heights.reduce(function(a, b) { | |
| return a + b + 2 * margin; | |
| }, 0); | |
| mediaGallery.style.height = Math.round(height + 10) + 'px'; | |
| var width = 0; | |
| for (var i = 0, lenI = divs.length; i < lenI; i++) { | |
| var currentWidth = | |
| parseInt(divs[i].style.width.replace('px', ''), 10); | |
| if (width < size - currentWidth - i * margin) { | |
| width += currentWidth; | |
| } else { | |
| if (i < lenI) { | |
| width = size + margin; | |
| } | |
| width += margin; | |
| break; | |
| } | |
| } | |
| mediaGallery.style.width = width + 'px'; | |
| if (DUMP_MEDIA_GALLERIES) { | |
| illustrator.createMediaGalleryDump(mediaItems, divs, width, height, | |
| algorithm, searchTerms, wikipediaUrl); | |
| } | |
| selectedMediaItems = null; | |
| mediaItems = null; | |
| return callback(container.innerHTML); | |
| } | |
| }, | |
| looseOrder: { | |
| name: 'Loose order, varying size', | |
| func: function(selectedMediaItems, mediaItems, algorithm, searchTerms, | |
| wikipediaUrl, callback) { | |
| if (selectedMediaItems.length === 0) { | |
| return callback(false); | |
| } | |
| // media gallery algorithm credits to | |
| // http://blog.vjeux.com/2012/image/- | |
| // image-layout-algorithm-facebook.html | |
| var heights = []; | |
| var columnSize = illustrator.mediaItemHeight; | |
| var size = illustrator.mediaGalleryWidth; | |
| var margin = illustrator.mediaGalleryMargin; | |
| var createColumns = function(n) { | |
| for (var i = 0; i < n; ++i) { | |
| heights[i] = 0; | |
| } | |
| }; | |
| var getMinColumn = function() { | |
| var minHeight = Infinity; | |
| var iMin = -1; | |
| for (var i = 0, len = heights.length; i < len; ++i) { | |
| if (heights[i] < minHeight) { | |
| minHeight = heights[i]; | |
| iMin = i; | |
| } | |
| } | |
| return iMin; | |
| }; | |
| var addColumnDiv = function(i, div, isBig) { | |
| var width = isBig ? columnSize * 2 + margin : columnSize; | |
| var height = isBig ? columnSize * 2 + margin : columnSize; | |
| div.setAttribute('style', | |
| 'margin-left:' + (margin + (columnSize + margin) * i) + 'px; ' + | |
| 'margin-top:' + ((columnSize + margin) * | |
| heights[Math.floor(i / 2)]) + 'px; ' + | |
| 'height:' + height + 'px; ' + | |
| 'width:' + width + 'px;'); | |
| var mediaItem = div.firstChild.firstChild; | |
| var mediaItemWidth = parseInt(mediaItem.width, 10); | |
| var mediaItemHeight = parseInt(mediaItem.height, 10); | |
| var aspectRatio = mediaItemWidth / mediaItemHeight; | |
| var min = Math.min(mediaItemHeight, mediaItemWidth); | |
| if (min === mediaItemWidth) { | |
| mediaItem.style.width = width + 'px'; | |
| mediaItem.style.height = (width / aspectRatio) + 'px'; | |
| } else { | |
| mediaItem.style.height = height + 'px'; | |
| mediaItem.style.width = (height * aspectRatio) + 'px'; | |
| } | |
| }; | |
| var calculateSizes = function(divs) { | |
| var nColumns = Math.floor(size / (2 * (columnSize + margin))); | |
| createColumns(nColumns); | |
| var smallDivs = []; | |
| var column; | |
| for (var i = 0, len = divs.length; i < len; ++i) { | |
| var div = divs[i]; | |
| column = getMinColumn(); | |
| var posterUrl = div.getAttribute('data-posterurl'); | |
| var mediaItem = mediaItems[posterUrl]; | |
| var resolutionGoodEnough = | |
| (mediaItem.fullImage.width * mediaItem.fullImage.height) > | |
| (illustrator.mediaItemHeight * illustrator.mediaItemHeight) ? | |
| true : false; | |
| var isBig = (Math.random() > 0.7) && (resolutionGoodEnough); | |
| if (isBig) { | |
| addColumnDiv(column * 2, div, true); | |
| heights[column] += 2; | |
| } else { | |
| smallDivs.push(div); | |
| if (smallDivs.length === 2) { | |
| addColumnDiv(column * 2, smallDivs[0], false); | |
| addColumnDiv(column * 2 + 1, smallDivs[1], false); | |
| heights[column] += 1; | |
| smallDivs = []; | |
| } | |
| } | |
| } | |
| if (smallDivs.length) { | |
| column = getMinColumn(); | |
| addColumnDiv(column * 2, smallDivs[0], false); | |
| heights[column] += 1; | |
| } | |
| }; | |
| var fragment = document.createDocumentFragment(); | |
| var divs = []; | |
| selectedMediaItems.forEach(function(item) { | |
| var div = document.createElement('div'); | |
| div.setAttribute('class', 'mediaItem photoBorder'); | |
| div.setAttribute('style', 'position:absolute; overflow:hidden;'); | |
| div.setAttribute('data-posterurl', item.posterUrl); | |
| div.setAttribute('data-faviconurl', | |
| item.origin.toLowerCase() + '.png'); | |
| var anchor = document.createElement('a'); | |
| anchor.href = item.micropostUrl; | |
| anchor.setAttribute('target', '_blank'); | |
| div.appendChild(anchor); | |
| var img = document.createElement('img'); | |
| if (item.type === 'photo') { | |
| img.src = item.mediaUrl; | |
| } else { | |
| img.src = item.posterUrl; | |
| } | |
| img.width = item.fullImage.width; | |
| img.height = item.fullImage.height; | |
| anchor.appendChild(img); | |
| var favicon = document.createElement('img'); | |
| favicon.setAttribute('class', 'favicon'); | |
| favicon.src = item.origin.toLowerCase() + '.png'; | |
| div.appendChild(favicon); | |
| divs.push(div); | |
| }); | |
| var mediaGallery = document.createElement('div'); | |
| calculateSizes(divs); | |
| divs.forEach(function(div) { | |
| fragment.appendChild(div); | |
| }); | |
| mediaGallery.appendChild(fragment); | |
| var highestColumn = Math.max.apply(Math, heights); | |
| var height = highestColumn * (illustrator.mediaItemHeight + margin); | |
| var columnIndex = -1; | |
| while (heights[++columnIndex]) { | |
| // no op | |
| } | |
| /* jshint noempty:false */ | |
| var width = 2 * columnIndex * (illustrator.mediaItemHeight + margin) + | |
| margin; | |
| mediaGallery.style.height = height + 'px'; | |
| var container = document.createElement('div'); | |
| container.appendChild(mediaGallery); | |
| if (DUMP_MEDIA_GALLERIES) { | |
| illustrator.createMediaGalleryDump(mediaItems, divs, width, height, | |
| algorithm, searchTerms, wikipediaUrl); | |
| } | |
| selectedMediaItems = null; | |
| mediaItems = null; | |
| return callback(container.innerHTML); | |
| } | |
| } | |
| }, | |
| createMediaGalleryDump: function(mediaItems, divs, width, height, algorithm, | |
| searchTerms, wikipediaUrl) { | |
| var len = divs.length; | |
| var margin = illustrator.mediaGalleryMargin; | |
| var fontSize = illustrator.mediaGalleryFontSize; | |
| var canvas = new Canvas(width, height + 25 + len * (fontSize + 3)); | |
| var ctx = canvas.getContext('2d'); | |
| ctx.fillStyle = 'white'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| var marginLeft = margin; | |
| var marginTop = margin; | |
| var imagesPerRow = 0; | |
| for (var i = 0; i < len; i++) { | |
| imagesPerRow++; | |
| var parentDiv = divs[i]; | |
| var posterUrl = parentDiv.getAttribute('data-posterurl'); | |
| var faviconUrl = parentDiv.getAttribute('data-faviconurl'); | |
| var imageWidth = parseInt(parentDiv.style.width.replace('px', ''), 10); | |
| var imageHeight = parseInt(parentDiv.style.height.replace('px', ''), 10); | |
| var dw = imageWidth; | |
| var dh = imageHeight; | |
| var dx = parseInt(parentDiv.style.marginLeft.replace('px', ''), 10) || | |
| marginLeft; | |
| var dy = parseInt(parentDiv.style.marginTop.replace('px', ''), 10) || | |
| marginTop; | |
| var img = mediaItems[posterUrl].fullImage.image; | |
| if (marginLeft < width - imageWidth - imagesPerRow * margin) { | |
| marginLeft += imageWidth + margin; | |
| } else { | |
| marginLeft = 0; | |
| imagesPerRow = 0; | |
| marginTop += imageHeight + margin; | |
| } | |
| var sw; | |
| var sh; | |
| var aspectRatio = img.width / img.height; | |
| if (aspectRatio > 1 /* landscape */) { | |
| sw = img.height; | |
| sh = img.height; | |
| } else { | |
| sw = img.width; | |
| sh = img.width; | |
| } | |
| try { | |
| ctx.drawImage(img, 0, 0, sw, sh, dx, dy + margin, dw, dh); | |
| } catch(e) { | |
| return console.log('Canvas error ' + e); | |
| } | |
| img = null; | |
| var favicon; | |
| if (!illustrator.faviconCache[faviconUrl]) { | |
| favicon = new Image(); | |
| favicon.src = __dirname + '/static/' + faviconUrl; | |
| illustrator.faviconCache[faviconUrl] = favicon; | |
| } else { | |
| favicon = illustrator.faviconCache[faviconUrl]; | |
| } | |
| try { | |
| ctx.drawImage(favicon, dx + 5, dy + margin + 5); | |
| } catch(e) { | |
| return console.log('Canvas error ' + e); | |
| } | |
| favicon = null; | |
| ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)'; | |
| ctx.lineWidth = 1; | |
| ctx.strokeRect(dx + 1, dy + margin + 1, dw - 1, dh - 1); | |
| var micropostUrl = mediaItems[posterUrl].micropostUrl; | |
| ctx.strokeStyle = 'white'; | |
| ctx.fillStyle = 'black'; | |
| ctx.lineWidth = 3; | |
| ctx.font = fontSize + 'pt Helvetica'; | |
| var index = i + 1; | |
| ctx.strokeText(index, dx + 23, dy + margin + 16, dw - 1); | |
| ctx.fillText(index, dx + 23, dy + margin + 16, dw - 1); | |
| ctx.fillText('[' + index + '] Source: ' + micropostUrl, margin, | |
| height + 5 * margin + i * (fontSize + 3), width); | |
| } | |
| canvas.toBuffer(function(err, buf) { | |
| var fileName = __dirname + '/mediagalleries/mediagallery_' + | |
| algorithm + '_' + Object.keys(searchTerms)[0] | |
| .replace(/\s/g, '_').replace(/\//g, '_') + | |
| '_' + Date.now() + '.png'; | |
| require('fs').writeFile(fileName, buf, function(err) { | |
| if (err) { | |
| return console.log('File write error: "' + fileName + '". Error: ' + | |
| err); | |
| } | |
| // if we have already tweeted the current URL, don't tweet it again | |
| if (recentTweetsBuffer.indexOf(wikipediaUrl) !== -1) { | |
| console.log('Already tweeted media gallery about ' + wikipediaUrl); | |
| return; | |
| } | |
| // keep the recent tweets buffer at most 10 elements long | |
| recentTweetsBuffer.push(wikipediaUrl); | |
| if (recentTweetsBuffer.length > 10) { | |
| recentTweetsBuffer.shift(); | |
| } | |
| twitterRestClient.statusesUpdateWithMedia({ | |
| 'status': '#BreakingNews candidate via @WikiLiveMon: ' + | |
| wikipediaUrl + '. Media gallery: ', | |
| 'media[]': fileName.replace(/^~/g, '/Users/tsteiner') | |
| }, | |
| function(error, result) { | |
| if (error) { | |
| console.log('Error: ' + (error.code ? error.code + ' ' + | |
| error.message : error.message)); | |
| } | |
| if (result) { | |
| console.log(result); | |
| } | |
| } | |
| ); | |
| }); | |
| }); | |
| mediaItems = null; | |
| } | |
| }; | |
| module.exports = illustrator.searchMediaItems; |