I am attempting to set multiple cue points in a video. Instead of writing each cue out, I would like to iterate over an object that has the data I need, such as the time of the cue and some info about what to do with the call back.
The problem is when I try to iterate over the object it overwrites all the cues except for the last one.
var products = myVideo.products;
var video = Popcorn('#mainVideo');
for (product in products){
var obj = products[product],
start = obj.start,
img = obj.image,
$targetDiv = $("#"+obj.targetDiv);
video.cue( start, function(){
$('<img>', {
class : "isImage",
src : 'images/' + img
}).prependTo( $targetDiv );
})
}
Any help would be greatly appreciated.
In this code, every cue has its own callback function, but every function refers to the same variable img and the same $targetDiv. By the time they run, those variables will be set to their respective values for the last item in products.
If you've ever run code through jslint and seen the warning, don't make functions in a loop, this is why. A good way to do what you're trying to do is to put those variables inside of a function that gets called immediately and returns another function, which is your callback. Like this:
function makeCallback(obj) {
return function() {
$('<img>', {
class : "isImage",
src : 'images/' + obj.img
}).prependTo( $("#"+obj.targetDiv) );
};
}
for (var product in products) {
var obj = products[product];
video.cue( obj.start, makeCallback( obj ) );
}
Alternatively, you can use forEach, which does the same thing under the hood. (Popcorn provides its own version, which handles both arrays and objects.)
Popcorn.forEach(products, function(obj) {
video.cue( start, function(){
$('<img>', {
class : "isImage",
src : 'images/' + obj.img
}).prependTo( $("#"+obj.targetDiv) );
});
});
I should note that you have another problem in this code, which is that you have Popcorn creating a new image every time you pass through the cue point. So, if the user ever skips back to replay some of the video, the images will start to pile up. Also, the images don't start loading until right when they become visible, so if there's a slow-ish network connection, the images may not show up until it's too late.
The best way to handle these is usually to create your images ahead of time but make them invisible with CSS, and then have the Popcorn events make them visible at the right time. You might consider using the image plugin, which will do most of your heavy lifting for you.
Related
I need to add an overlay to an existing OpenSeadragon viewer object which isn't created by my code, but elsewhere in the application.
I have got to a point where I know that the viewer has been created as I can access the various html elements that are created via jQuery. However I can't work out if there's any way to create a viewer from an existing reference.
I've tried using the id of the viewer div in:
var viewer = OpenSeadragon(id: "open-seadragon-viewer-id");
but this doesn't seem to work.
Is there any way to do this or can you only get the viewer within the code that initialised it?
Here's one crazy thought... you could monkey-patch some portion of OSD in order to grab the viewer...
var viewer;
var originalIsOpen = OpenSeadragon.Viewer.prototype.isOpen;
OpenSeadragon.Viewer.prototype.isOpen = function() {
// Now we know the viewer!
viewer = this;
// Reinstate the original, since we only need to run our version once
OpenSeadragon.Viewer.prototype.isOpen = originalIsOpen;
// Call the original
return originalIsOpen.call(this);
}
It's kind of tacky, but should work. Note this assumes there is only one viewer on the page... if there are more than one, the same principle could work but you would need to keep track of an array of viewers.
BTW, I'm using isOpen, because it's simple and it gets called every frame. Other functions could work as well.
EDIT: fixed code so we are using the prototype. I still haven't actually tested this code so there may still be bugs!
This solution does not directly answer the question, as it relies on your own code creating the OpenSeaDragon object. It is an implementation of #iangilman's mention of storing the viewer in a global variable. However others may find it useful. (Note that passing a global variable to a function requires a workaround - see Passing a global variable to a function)
The code demonstrates how to use the same OpenSeaDragon object to display different pictures.
var viewer3=null; //global variable
var newURL1='image/imageToDisplay1.png';
var newURL2='image/imageToDisplay2.png';
var elementID='myID';
//the loadScan function will display the picture using openSeaDragon and can be called as many times as you want.
loadScan("viewer3",newURL1,elementID);
loadScan("viewer3",newURL2,elementID);
//the actual function
function loadScan(theViewer,newURL,theID) {
//if object has already been created, then just change the image
if (window[theViewer]!=null) {
window[theViewer].open({
type: 'image',
url: newURL
});
} else {
//create a new OpenSeadragon object
window[theViewer] = OpenSeadragon({
prefixUrl: "/myapp/vendor/openseadragon/images/",
id: theID,
defaultZoomLevel: 1,
tileSources: {
url: newURL,
type: 'image'
}
});
}
}
All of the assets of the site are in an array of objects that will be shown after user interaction, something like a slider.
{"id":"1","icon":"icon-name-class","sound":"soud-name"},
{"id":"2","icon":"icon-2-name-class","sound":"soud-2-name"},
....
{"id":"100","icon":"icon-100-name-class","sound":"soud-100-name"}
I have about 150 small icons and around 100 small sounds that need to be preloaded, been looking and didn't find anything related to preload assets from a js array.
I'm using the images as a background and getting the class name from the object.
I thought that all images would be ready since they are coming from a css class, but i guess that i am wrong
.icon-class-1 { background-image: url(../img/icons/img-1.png); }
this link or something similar to this could do the trick but after reading the doc couldn't find how to apply it when the assets origin is in an array that will be managed with js.
You have to loop through the object array to load them.
EDIT
Since you have a lot images which doesn't load via CSS.
I suggest you remove all these backgroung-image:url('...'); and put these URLs in your json like this:
{"id":"1","icon":"icon-name-class", "icon_url":"http://domain/path/file.jpg", "sound":"sound-name","sound_url":"http://domain/path/file.mp3"},
And do the same for your sound urls.
PLUS! It will ease the maintenance in the long run... If you notice a broken URL someday.
Then, preloading can be made like this:
var object_array = [
{"id":"1","icon":"icon-name-class", "icon_url":"http://domain/path/file1.jpg", "sound":"sound-name","sound_url":"http://domain/path/file1.mp3"},
{"id":"2","icon":"icon-name-class", "icon_url":"http://domain/path/file2.jpg", "sound":"sound-name","sound_url":"http://domain/path/file2.mp3"},
{"id":"3","icon":"icon-name-class", "icon_url":"http://domain/path/file3.jpg", "sound":"sound-name","sound_url":"http://domain/path/file3.mp3"}
];
// Preloading arrays
var icon_url_arr = [];
var sound_url_arr = [];
// Load counter
var loaded_counter={icon:0,sound:0};
function increment_counter(x){
loaded_counter[x]++;
}
// Loop through the data array
for(i=0;i<object_array.length;i++){
if(typeof(object_array[i].icon_url)!="undefined"){
icon_url_arr[i] = $("<img>").attr("src",object_array[i].icon_url).on("load",function(){increment_counter("icon");});
}
if(typeof(object_array[i].sound_url)!="undefined"){
var audio_element = $("<audio>").attr("id","audio_"+i);
sound_url_arr[i] = $("<source>").attr("src",object_array[i].sound_url).attr("type","audio/mpeg");
audio_element.append(sound_url_arr[i]).on("loadeddata",increment_counter("sound"));
$("#main").append(audio_element);
}
}
// Interval to check load status
var check_all_loaded = setInterval(function(){
console.log("Loded icons: " +loaded_counter.icon+ " Loaded sounds: " +loaded_counter.sound );
$("#icon").html((loaded_counter.icon/icon_url_arr.length)*100+"%");
$("#sound").html((loaded_counter.sound/sound_url_arr.length)*100+"%");
// Stop the interval when all loaded
if( (icon_url_arr.length==loaded_counter.icon) && (sound_url_arr.length==loaded_counter.sound) ){
clearInterval(check_all_loaded);
console.log("Load check Interval stopped.");
// Now you can use the loaded icons and sounds
cycle_data();
}
},10);
// cycle through data on interval
function cycle_data(){
console.log("Display Interval started.");
// Background
var i=0;
setInterval(function(){
$("#main").css("background-image","url("+icon_url_arr[i].attr("src")+")");
// Audio
$(document).find("audio").get(0).pause();
$(document).find("#audio_"+i).get(0).play();
// Increment loop counter.
i++;
// Reset counter when the array end is reached
if(i==icon_url_arr.length){
i=0;
}
},2000);
}
So it won't be displayed or played anywhere...
But the browser will have it loaded.
You may have to hide the audio players via CSS:
audio{
display:none;
}
I made a CodePen demo to make sure it is working.
There is only 3 items in json...
And images and sounds are pretty small.
So you won't really notice the load delay in this demo.
;)
I suggest you place this script in a document.ready() wrapper...
And within a setTimeout() function to execute it something like 0.5 to 2 seconds after document is ready.
So the rest of your page won't lag.
Here is my scenario. The page loads and the map loads with an empty vector layer. So its there, but has no features. Then the user clicks a button and a CQL filter loads features according to the CQL settings.
My methodology to implement this. I set an empty vector layer, no loader or strategy. The button the user clicks for the first time calls a "initialization " function (=firstTimeOnly()) that:
sets a loader and a strategy to the vector layer
now that a loader exists, calls another "filtering" function (=changeFilter()) that
resets the loader's cql filter and loads features
now the "filtering" function gets attached to the button and called
with every click. The "initialization" function served its purpose
and detaches itself from the button.
Here is my code
<button id= "magicButton", onclick="firstTimeOnly()">Click me</button>
//set globals to use them
var cqlFilter = "name='testpoint9'";
var urlTemplate = 'http://localhost:5550/geoserver/mymap/wfs?service=WFS&version=1.0.0&request=GetFeature&typeName=mymap:layerName&CQL_FILTER={{CQLFILTER}}&outputFormat=application/json';
var loader = function (extent) {
var url = urlTemplate.replace('{{CQLFILTER}}', cqlFilter);
$.ajax(url, {
type: 'GET',
success: function(response) {
var res = response;
var geojsonFormat = new ol.format.GeoJSON();
sourceVector.addFeatures(geojsonFormat.readFeatures(response));
}
})
};
var strategy = new ol.loadingstrategy.tile(ol.tilegrid.createXYZ({maxZoom: 20}));
//empty vector source
var sourceVector = new ol.source.Vector({});
function changeFilter() {
//remove all, set cql and reload
var featsToRemove = layerVector.getSource().getFeatures();
for (var f=0;f<featsToRemove.length;f++)
{
layerVector.getSource().removeFeature(featsToRemove[f]);
}
cqlFilter = "name LIKE 'p'";
sourceVector.clear(true);
}
layerVector = new ol.layer.Vector({
source: sourceVector,
style:styleFunction
});
function firstTimeOnly() {
sourceVector.set('loader', loader);
sourceVector.set('strategy', strategy);
changeFilter();
document.getElementById("magicButton").removeEventListener("click", firstTimeOnly, false);
document.getElementById("magicButton").addEventListener("click", changeFilter, false);
}
This is based to erilem's code for cql filter resetting and if I use just his code works fine. But if I want to start with an empty layer and edit it as the above, I get nothing. My code gives no errors. But, if I click the button I get nothing.
Please advide me how to fix this. Or maybe its an overkill and you suggest something smarter.
Thanks
UPDATE
If I put
console.log("loader "+sourceVector.get('loader'));
at the end of the changeFilter I get the loader function. So, I guess the first time I click the button, the firstTimeOnly actually sets a loader and calls changeFilter. So, the loader is there, but does not work? Any help?
Without getting into potential issues with Openlayers the issue I see is that removeEventListener only works for removing listeners that were set with addEventListener. Since you bound the onclick declaratively in the HTML the way to unbind it would be by doing document.getElementById("magicButton").onclick = null.
That said I would change your example so that somewhere in your code you set the event listener using addEventListener.
Example:
function firstTimeOnly() {
sourceVector.set('loader', loader);
sourceVector.set('strategy', strategy);
changeFilter();
document.getElementById("magicButton").removeEventListener("click", firstTimeOnly, false);
document.getElementById("magicButton").addEventListener("click", changeFilter, false);
}
document.getElementById("magicButton").addEventListener("click", firstTimeOnly, false);
And get rid of the onclick in the HTML.
Also consider caching the reference to magicButton so that you don't have to be constantly calling getElementById.
var magicButton = document.getElementById("magicButton");
The short description of the functionality that we are trying to achieve: we have a list of source objects on the left, a person can drag new items from the list to a list on the right, items thus get added to the list on the right; they can also remove items from the list on the right. The list on the right then gets saved whenever it is changed. (I don't think the specifics of how/where it is being saved matter...)
I am having a problem with a bit of timing in the JavaScript vs. DOM elements realm of things. Items that are already on the list on the right can be removed. We have some code that fires on a 'remove/delete' type icon/button on a DOM element, that is supposed to remove the element from the DOM visually and permanently (i.e. it doesn't need to be brought back with a 'show'). This visual change should then also show up in the JSON object that is built when the JS traverses the DOM tree to build the new updated list.
However, this chunk of JS code that runs immediately after this .remove() is called, the element that should have just been removed still shows up in the JSON object. This is not good.
Here are what I believe to be the relevant bits of code operating here. This lives in a web browser; much of this is in the document.ready() function. A given list can also have subsections, hence the sub-list parts and loops.
The on-click definition:
$('body').on('click', '.removeLine', function() {
var parent=$(this).parent().parent().parent(); //The button is a few DIVs shy of the outer container
var List=$(this).closest('article'); //Another parent object, containing all the
parent.fadeOut( 300,
function() {
parent.slideUp(300);
parent.remove();
}
);
sendList(List); // This builds and stores the list based on the DOM elements
});
And then later on, this function definition:
function sendList(List) {
var ListArray=[],
subListArray=[],
itemsArray = [],
subListName = "";
var ListTitle = encodeText(List.find('.title').html());
// loop through the subLists
List.find('.subList').each(
function(index, element) {
subListName=($(this).find('header > .title').html()); // Get sublist Title
subListID=($(this).attr('id')); // Get subList ID
// loop through the line items
itemsArray=[];
$(this).find('.itemSearchResult').each(
function(index, element) {
// Build item Array
if( $(this).attr('data-itemid')!= item ) {
itemArray.push( $(this).attr('data-itemid'));
}
}
);
// Build SubList Array with items Array
subListArray.push(
{
"subListName": subListName,
"subListID" : subListID,
"items" : itemsArray
}
);
}
); <!-- end SubList Loop -->
// Complete List Array with subListArray
ListArray ={
"ListName": ListTitle,
"ListID": List.attr('id'),
"subLists": subListArray
};
// Send New List to DataLists Object - the local version of storage
updateDataLists(ListArray);
// Update remote storage
window.location= URLstring + "&$Type=List" + "&$JSON=" + JSON.stringify(ListArray) + "&$objectID=" + ListArray.ListID;
};
It seems to be the interaction of the 'parent.remove()' step and then the call to 'sendList()' that get their wires crossed. Visually, the object on screen looks right, but if we check the data being sent to the storage, it comes through WITH the object that was visually removed.
Thanks,
J
PS. As you can probably tell, we are new at the Javascript thing, so our code may not be terribly efficient or proper. But...it works! (Well, except for this issue. And we have run into this issue a few times. We have a workaround for it, but I would rather understand what is going on here. Learn the deeper workings of JS so we don't create these problems in the first place.)
There's a few things going on here, but I'm going to explain it by approaching it from an asynchronous programming perspective.
You are calling sendList before the element gets removed from the DOM. Your element doesn't get removed from the DOM until after your fadeOut callback gets executed (which takes 300ms).
Your sendList function gets called immediately after you begin the fadeOut, but your program doesn't wait to call sendList until your fadeOut is finished - that's what the callback is for.
So I would approach it by calling sendList in the callback, after your DOM element has been removed like this:
$('body').on('click', '.removeLine', function() {
var el = $(this); //maintain a reference to $(this) to use in the callback
var parent=$(this).parent().parent().parent(); //The button is a few DIVs shy of the outer container
parent.fadeOut( 300,
function() {
parent.slideUp(300);
parent.remove();
sendList(el.closest('article'));
}
);
});
No idea what I'm doing or why it isn't working. Clearly not using the right method and probably won't use the right language to explain the problem..
Photogallery... Trying to have a single html page... it has links to images... buttons on the page 'aim to' modify the path to the images by finding the name currently in the path and replacing it with the name of the gallery corresponding to the button the user clicked on...
example:
GALLERY2go : function(e) {
if(GalleryID!="landscapes")
{
var find = ''+ findGalleryID()+'';
var repl = "landscapes";
var page = document.body.innerHTML;
while (page.indexOf(find) >= 0) {
var i = page.indexOf(find);
var j = find.length;
page = page.substr(0,i) + repl + page.substr(i+j);
document.body.innerHTML = page;
var GalleryID = "landscapes";
}
}
},
There's a function higher up the page to get var find to take the value of var GalleryID:
var GalleryID = "portfolio";
function findGalleryID() {
return GalleryID
}
Clearly the first varGalleryID is global (t'was there to set a default value should I have been able to find a way of referring to it onLoad) and the one inside the function is cleared at the end of the function (I've read that much). But I don't know what any of this means.
The code, given its frailties or otherwise ridiculousness, actually does change all of the image links (and absolutely everything else called "portfolio") in the html page - hence "portfolio" becomes "landscapes"... the path to the images changes and they all update... As a JavaScript beginner I was pretty chuffed to see it worked. But you can't click on another gallery button because it's stuck in a loop of some sort. In fact, after you click the button you can't click on anything else and all of the rest of the JavaScript functionality is buggered. Perhaps I've introduced some kind of loop it never exits. If you click on portfolio when you're in portfolio you crash the browser! Anyway I'm well aware that 'my cobbled together solution' is not how it would be done by someone with any experience in writing code. They'd probably use something else with a different name that takes another lifetime to learn. I don't think I can use getElement by and refer to the class/id name and parse the filename [using lots of words I don't at all understand] because of the implications on the other parts of the script. I've tried using a div wrapper and code to launch a child html doc and that come in without disposing of the existing content or talking to the stylesheet. I'm bloody lost and don't even know where to start looking next.
The point is... And here's a plea... If any of you do reply, I fear you will reply without the making the assumption that you're talking to someone who really hasn't got a clue what AJAX and JQuery and PHP are... I have searched forums; I don't understand them. Please bear that in mind.
I'll take a stab at updating your function a bit. I recognize that a critique of the code as it stands probably won't help you solve your problem.
var currentGallery = 'landscape';
function ChangeGallery(name) {
var imgs = document.getElementsByTagName("img") // get all the img tags on the page
for (var i = 0; i < imgs.length; i++) { // loop through them
if (imgs[i].src.indexOf(currentGallery) >= 0) { // if this img tag's src contains the current gallery
imgs[i].src = imgs[i].src.replace(currentGallery, name);
}
}
currentGallery = name;
}
As to why I've done what I've done - you're correct in that the scope of the variables - whether the whole page, or only the given function, knows about it, is mixed in your given code. However, another potential problem is that if you replace everything in the html that says 'landscape' with 'portfolio', it could potentially change non-images. This code only finds images, and then replaces the src only if it contains the given keyword.