Skip to content

Instantly share code, notes, and snippets.

@joebrislin
Last active December 10, 2015 07:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joebrislin/d06a77fb3f77df99c01c to your computer and use it in GitHub Desktop.
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)
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;
}
<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>
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
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;
}
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