Last active
December 10, 2015 07:22
-
-
Save joebrislin/d06a77fb3f77df99c01c to your computer and use it in GitHub Desktop.
Code Samples - Interactive Map displaying Videos within the State of VA (Ruby on Rails / KnockoutJS)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var map, markers, geoXml, mc; | |
var defaultZoom = 7; | |
var maxZoom = 20; | |
var loadModal = function(elem, evt){ | |
evt.preventDefault(); | |
$("#videoModal").modal({remote: $(elem).attr('href')}); | |
}; | |
$(".explore-index").ready( function(){ | |
function initialize() { | |
var mstyle = [ | |
{ | |
"featureType": "water", | |
"stylers": [ | |
{ "saturation": -100 }, | |
{ "visibility": "on" }, | |
{ "lightness": -40 } | |
] | |
},{ | |
"featureType": "landscape", | |
"stylers": [ | |
{ "visibility": "on" }, | |
{ "saturation": -100 }, | |
{ "lightness": -16 } | |
] | |
},{ | |
"featureType": "road.highway", | |
"stylers": [ | |
{ "saturation": -100 } | |
] | |
},{ | |
"featureType": "poi", | |
"stylers": [ | |
{ "visibility": "simplified" }, | |
{ "saturation": -100 }, | |
{ "lightness": -16 } | |
] | |
},{ | |
"featureType": "road", | |
"elementType": "labels", | |
"stylers": [ | |
{ "visibility": "off" } | |
] | |
} | |
]; | |
var clusterStyles = [ | |
{ | |
textColor: 'black', | |
url: '/map/marker_small.svg', | |
height: 50, | |
width: 50 | |
}, | |
{ | |
textColor: 'black', | |
url: '/map/marker_med.svg', | |
height: 75, | |
width: 75 | |
}, | |
{ | |
textColor: 'black', | |
url: '/map/marker_large.svg', | |
height: 100, | |
width: 100 | |
} | |
]; | |
var infoBubbleOptions = { | |
shadowStyle: 1, | |
padding: 0, | |
backgroundColor: '#D9D3C9', | |
borderRadius: 3, | |
arrowSize: 10, | |
borderWidth: 3, | |
borderColor: '#D9D3C9', | |
disableAutoPan: false, | |
hideCloseButton: false, | |
arrowPosition: 40, | |
backgroundClassName: 'transparent', | |
arrowStyle: 0, | |
minWidth: 265, | |
minHeight: 206 | |
}; | |
if(detectIE() != false){ | |
infoBubbleOptions['minWidth'] = 273; | |
infoBubbleOptions['minHeight'] = 206; | |
} | |
if( window.innerWidth > 1600 ){ | |
defaultZoom = 8; | |
} | |
var mapOptions = { | |
zoom: defaultZoom, | |
center: new google.maps.LatLng(37.4883333, -78.5633333), | |
mapTypeControl: false, | |
mapTypeControlOptions: { | |
mapTypeId: [google.maps.MapTypeId.ROADMAP,'noroads'] | |
}, | |
panControl: false, | |
streetViewControl: false, | |
zoomControl: false, | |
zoomControlOptions: { | |
style: google.maps.ZoomControlStyle.SMALL, | |
position: google.maps.ControlPosition.BOTTOM_LEFT | |
} | |
}; | |
map = new google.maps.Map(document.getElementById('map-canvas'),mapOptions); | |
geoXml = new geoXML3.parser({ | |
map: map, | |
suppressInfoWindows: true, | |
zoom: false | |
}); | |
geoXml.parse('/map/virginia_inverted_kml.xml'); | |
var styledMapOptions = {name: "No Roads"} | |
var styledMap = new google.maps.StyledMapType(mstyle,styledMapOptions); | |
map.mapTypes.set('noroads', styledMap); | |
map.setMapTypeId('noroads'); | |
markers = []; | |
var ib = new InfoBubble(infoBubbleOptions); | |
$.ajax({ | |
type: "POST", | |
url: "/api/v1/search", | |
data: { | |
limit: 25000, | |
hascoords: true | |
}, | |
dataType: "json" | |
}).done(function( resp, textStatus, jqXHR ){ | |
if(resp.success && resp.data instanceof Array && resp.data.length > 0) { | |
for (var i = 0; i < resp.data.length; i++) { | |
var _video = resp.data[i]; | |
var myLatlng = new google.maps.LatLng(_video.latitude, _video.longitude); | |
var marker = new google.maps.Marker({ | |
position: myLatlng, | |
data: _video, | |
icon: '/map/marker.svg' | |
}); | |
google.maps.event.addListener(marker, 'click', function() { | |
var _template = '<ul><li><a href="/video/' + this.data.youtubeid + '" onClick="loadModal(this, event)" class="infowindowvideo">' + | |
'<figure>' + | |
'<img src="//img.youtube.com/vi/' + this.data.youtubeid + '/mqdefault.jpg" alt="'+this.data.name+'" />' + | |
'</figure>' + | |
'<div class="overlay">' + | |
'<div class="playBtn"><i class="fa fa-play-circle"></i></div>' + | |
'<div class="titleBar">' + | |
'<div class="videoName">' + this.data.name + '</div>' + | |
'<div class="videoTime">' + this.data.length + '</div>' + | |
'</div>' + | |
'</div>' + | |
'</a></li></ul>'; | |
ib.setContent(_template); | |
ib.open(map, this); | |
}); | |
markers.push(marker); | |
} | |
mc = new MarkerClusterer(map, markers, { | |
styles: clusterStyles, | |
maxZoom: 17, | |
calculator: function(markers, numStyles) { | |
// Custom style can be returned here | |
return { | |
text: markers.length, | |
index: numStyles | |
}; | |
} | |
}); | |
} | |
}).fail(function( jqXHR, textStatus, errorThrown ){ | |
}); | |
$("#zoomout").addClass("disabled"); | |
google.maps.event.addListener(map, 'zoom_changed', function(){ | |
var currentZoomLevel = map.getZoom(); | |
if(currentZoomLevel == defaultZoom){ | |
$('#zoomout').addClass("disabled"); | |
}else{ | |
$('#zoomout').removeClass("disabled"); | |
} | |
if(currentZoomLevel == maxZoom){ | |
$('#zoomin').addClass("disabled"); | |
}else{ | |
$('#zoomin').removeClass("disabled"); | |
} | |
}); | |
} | |
if( typeof google !== 'undefined' ){ | |
google.maps.event.addDomListener(window, 'load', initialize); | |
$(function() { | |
$('#zoomout').on('click',function(){ | |
var currentZoomLevel = map.getZoom(); | |
if(currentZoomLevel != 7){ | |
map.setZoom(currentZoomLevel - 1); | |
} | |
// toggleZoomControls(); | |
}); | |
$('#zoomin').on('click',function(){ | |
var currentZoomLevel = map.getZoom(); | |
if(currentZoomLevel != maxZoom){ | |
map.setZoom(currentZoomLevel + 1); | |
} | |
// toggleZoomControls(); | |
}); | |
$('#map-footer .close').on('click',function(){ | |
$("#zoom-controls").addClass('nofooter'); | |
$(this).parent().slideToggle('slow'); | |
}); | |
}); | |
} | |
}); | |
function detectIE() { | |
var ua = window.navigator.userAgent; | |
var msie = ua.indexOf('MSIE '); | |
if (msie > 0) { | |
// IE 10 or older => return version number | |
return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10); | |
} | |
var trident = ua.indexOf('Trident/'); | |
if (trident > 0) { | |
// IE 11 => return version number | |
var rv = ua.indexOf('rv:'); | |
return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10); | |
} | |
var edge = ua.indexOf('Edge/'); | |
if (edge > 0) { | |
// IE 12 => return version number | |
return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10); | |
} | |
// other browser | |
return false; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div id="map-canvas"></div> | |
<div id="zoom-controls"> | |
<button id="zoomout"><i class="fa fa-minus"></i></button> | |
<span class="divider"></span> | |
<button id="zoomin"><i class="fa fa-plus"></i></button> | |
</div> | |
<div id="map-footer"> | |
<button type="button" class="close" aria-label="Close"><i class="fa fa-times"></i></button> | |
<h3 class="h1">Every Virginian Has a Story</h3> | |
<p>To watch individual videos submitted to <a href="#aboutModal" data-toggle="modal">Virginia Voices</a>, begin by selecting an area in yellow and then choosing an icon with a camera. You can also search for individual stories or topics <a href="#searchModal" data-toggle="modal">here</a>.</p> | |
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Api | |
module V1 | |
class SearchController < BaseController | |
# Get Method | |
def index | |
_ret = Hash.new; | |
_ret[:data] = nil; | |
_ret[:message] = 'Results not found.'; | |
_ret[:success] = false; | |
_ret[:data] = Video.take(20) | |
_ret[:message] = ''; | |
_ret[:success] = true; | |
render json: _ret | |
end | |
# Post Method | |
def create | |
_ret = Hash.new; | |
_ret[:data] = nil; | |
_ret[:message] = 'No results found using search criteria.'; | |
_ret[:success] = false; | |
_qry = Hash.new; | |
_qry["nearme"] = false; | |
_qry["hascoords"] = false; | |
_qry["lat"] = nil; | |
_qry["long"] = nil; | |
_qry["keyword"] = nil; | |
_qry["limit"] = 20; | |
_qry["offset"] = nil; | |
if (params.has_key?(:nearme) && params[:long].to_s == "true") | |
_qry["nearme"] = true; | |
end | |
if (params.has_key?(:lat) && params.has_key?(:long) && params[:lat].to_s.length > 0 && params[:long].to_s.length > 0) | |
_qry["lat"] = params[:lat].to_s | |
_qry["long"] = params[:long].to_s | |
end | |
if (params.has_key?(:hascoords) && params[:hascoords].to_s == "true") | |
_qry["hascoords"] = true; | |
end | |
if (params.has_key?(:keyword) && params[:keyword].to_s.length > 0) | |
_qry["keyword"] = params[:keyword].to_s | |
end | |
if (params.has_key?(:limit) && params[:limit].to_i > 0) | |
_qry["limit"] = params[:limit].to_i | |
end | |
if (params.has_key?(:offset) && params[:offset].to_i >= 0) | |
_qry["offset"] = params[:offset].to_i | |
end | |
if(_qry["nearme"] && _qry["lat"] != nil && _qry["long"] != nil) | |
_ret[:data] = Video.where("longitude IS NOT NULL AND latitude IS NOT NULL").order("Point(longitude, latitude) <-> Point("+_qry["long"]+", "+_qry["lat"]+")").offset(_qry["offset"]).limit(_qry["limit"]) | |
_ret[:message] = ''; | |
_ret[:success] = true; | |
elsif(_qry["hascoords"]) | |
_ret[:data] = Video.where("longitude IS NOT NULL AND latitude IS NOT NULL").offset(_qry["offset"]).limit(_qry["limit"]) | |
_ret[:message] = ''; | |
_ret[:success] = true; | |
elsif(_qry["keyword"] != nil && _qry["keyword"].length > 0) | |
_videos = Video.where("LOWER(name) LIKE LOWER(:keyword) OR LOWER(description) LIKE LOWER(:keyword)", {keyword: "%"+_qry["keyword"]+"%"}) | |
_ret[:count] = _videos.count | |
_ret[:data] = _videos.offset(_qry["offset"]).limit(_qry["limit"]) | |
_ret[:message] = ''; | |
_ret[:success] = true; | |
else | |
_videos = Video; | |
_ret[:count] = _videos.count | |
_ret[:data] = _videos.offset(_qry["offset"]).limit(_qry["limit"]) | |
_ret[:message] = ''; | |
_ret[:success] = true; | |
end | |
if(_ret[:data] == nil || _ret[:data].length == 0) | |
_ret[:message] = 'No results found using search criteria.'; | |
_ret[:success] = true; | |
end | |
render json: _ret | |
end | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var VideoModel = function () { | |
var self = this; | |
self.isLoading = ko.observable(false); | |
self.pages = ko.observable(0); | |
self.currentPage = ko.observable(1); | |
self.suggestedVideos = ko.observableArray([]); | |
self.suggestedVideosMobile = ko.observableArray([]); | |
self.videoResults = ko.observableArray([]); | |
self.load = function() { | |
// Important: we need to load countries and languages first and only then load the user, to make | |
// sure that corresponding drop-downs are populated before loaded user will try to set values on them | |
// loadUser() is run once, after both loadCountries() and loadLanguages() have run (they do run in parallel!) | |
var parallelExecutions = [self.loadSuggestedVideos]; | |
// var delayedLoadUser = _.after(parallelExecutions.length, self.loadUser); | |
_.each(parallelExecutions, function(func) { | |
func(); | |
}); | |
}; | |
// Load all available videos | |
self.loadSuggestedVideos = function(ctx) { | |
// Let loading indicator know that there's a new loading task that ought to complete | |
self.isLoading(true); | |
$.getJSON('/api/v1/suggestedtitles/', | |
function(ret) { | |
$.each(ret.data, function(i, obj) { | |
// obj = ret.data; | |
obj['descLimited'] = limitText( obj['description'] ); | |
self.suggestedVideos.push(obj); | |
}); | |
self.suggestedVideosMobile( ret.data.slice(0,6) ); | |
// Let loading indicator know that this task has been completed | |
self.isLoading(false); | |
// Try to call success callback, which is loadUser if and only if all parallel processes have completed | |
if (ctx && ctx.success !== 'undefined') {ctx.success();} | |
} | |
); | |
} | |
self.searchVideos = function( page, keyword ) { | |
// Let loading indicator know that there's a new loading task that ought to complete | |
self.isLoading(true); | |
if( page != null ) { | |
self.currentPage(page); | |
}else{ | |
page = 1; | |
} | |
var data = { | |
'limit' : 10, | |
'offset' : (page - 1) * 10 | |
}; | |
if( keyword !== undefined ){ | |
data['keyword'] = keyword; | |
} | |
self.videoResults([]); // reset array | |
$.post('/api/v1/search', data, function(ret) { | |
// This callback is executed if the post was successful | |
$.each(ret.data, function(i, obj) { | |
// obj = ret.data; | |
obj['descLimited'] = limitText( obj['description'] ); | |
self.videoResults.push(obj); | |
}); | |
self.pages( Math.ceil(ret.count / 10) ); | |
// Let loading indicator know that this task has been completed | |
self.isLoading(false); | |
}); | |
} | |
this.loadTile = function(elem,i) { if (elem.nodeType === 1) $(elem).hide().delay((i) * 250).fadeIn(900); } | |
this.runResponsivePagination = function(elements, data) { | |
if(this.foreach[this.foreach.length-1] === data) | |
$(".pagination").rPage(); | |
}; | |
} | |
var videoModel = new VideoModel(); | |
$(function() { | |
// LOAD DATA | |
ko.applyBindings(videoModel); | |
// Load initial data via Ajax | |
videoModel.load(); | |
}); | |
function limitText(str){ | |
var maxLength = 150; | |
var textString = str + ' '; | |
var textLimit = textString.substring(0,maxLength+1); | |
textLimit = textLimit.substr(0, Math.min(textLimit.length, textLimit.lastIndexOf(" "))); | |
if(str.length > maxLength) textLimit += " ..."; | |
return textLimit; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Api | |
module V1 | |
class YoutubeController < BaseController | |
before_filter :set_variables | |
# Requires | |
require 'youtube_helper' | |
# Instance Variables | |
def set_variables | |
@per_page = 50 | |
@channelID = 'UCIw_-Pr1x0i1paCk5G6SgHQ' | |
@user = 'UCIw_-Pr1x0i1paCk5G6SgHQ' | |
# @ytClient = YouTubeIt::Client.new(:dev_key => YouTubeITConfig.dev_key) | |
@yt_auth = YoutubeHelper.new(YouTubeITConfig.client_id,YouTubeITConfig.client_secret,YouTubeITConfig.refresh_token).authorize | |
end | |
def index | |
videos = getVideos() | |
ActiveRecord::Base.transaction do | |
Video.delete_all("id > 0"); | |
videos.each do |v| | |
video = Video.new | |
video.youtubeid = v.id | |
video.name = v.title | |
video.length = v.duration | |
video.description = v.description | |
video.latitude = v.location["latitude"] | |
video.longitude = v.location["longitude"] | |
unless video.save | |
raise ActiveRecord::Rollback | |
end | |
end | |
end | |
test = Video.first(5) | |
render json: test | |
end | |
def getVideos() | |
channel = Yt::Channel.new id: @channelID, auth: @yt_auth | |
return channel.videos | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment