I am working on animating an OpenLayers map with multiple layers over a period of time. I would like to precache ol.layer.tile tiles to have smooth transitions between the dates. Any suggestions on how to precache/preload these tiles?
You'll want to rely on your browser cache here. And it requires that your server sends proper cache headers, so the browser does not re-fetch the images with every request. With these prerequisites in mind, proceed as follows:
Call ol.source.TileImage#getTileUrlFunction on your source so you can compute the urls of the tiles you want to cache.
Call ol.source.TileImage#getTileGrid on your source so you can get the tile coordinates for the extent and zoom level you want to cache
Call ol.tilegrid.TileGrid#forEachTileCoord with a function that computes the url for each tile and sets it as src on an image object. When doing so, keep track of the number of loading tiles so you know when you're done.
Repeat the above for all dimensions, after making the respective dimension changes to your source.
In the end, your code for preloading a single dimension could look something like this:
var loadingCount = 0;
var myTileUrlFunction = mySource.getTileUrlFunction();
var myTileGrid = mySource.getTileGrid();
myTileGrid.forEachTileCoord(myExtent, myZoom, function(tileCoord) {
var url = myTileUrlFunction.call(mySource, tileCoord, ol.has.DEVICE_PIXEL_RATIO, myProjection);
var img = new Image();
img.onload = function() {
--loadingCount;
if (loadingCount == 0) {
// ready to animate
}
}
++loadingCount;
img.src = url;
}
Solution that gets around cache-preventing headers.
var i = 0;
var renderer = new ol.renderer.canvas.TileLayer(layer);
var tileSource = layer.getSource();
var datePromise = new Promise(function(resolve, reject) {
tileGrid.forEachTileCoord(extent, currentZ, function(tileCoord) {
tile = tileSource.getTile(tileCoord[0], tileCoord[1], tileCoord[2], pixelRatio, projection);
tile.load();
var loader = function(e) {
if(e.type === 'tileloadend') {
--i;
if(i === 0) {
resolve();
}
} else {
reject(new Error('No response at this URL'));
}
/* remove listeners */
this.un('tileloadend', loader);
this.un('tileloaderror', loader);
};
tileSource.on('tileloadend', loader);
tileSource.on('tileloaderror', loader);
++i;
});
});
Related
I am using the following code to render DOM elements as canvas data and then make a PDF with them.
I have had to create a loop for this because if it was all one image it is impossible to correctly place the data over multiple pages.
$scope.pdfMaker = function() {
var content = [];
var pdfElement = document.getElementsByClassName("pdfElement");
for (var i = pdfElement.length - 1; i >= 0; i--) {
html2canvas(pdfElement[i], {
onrendered: function(canvas) {
var data = canvas.toDataURL();
content.push({
image: data,
width: 500,
margin: [0, 5]
});
console.log(canvas);
}
});
}
var docDefinition = {
content: content
};
setTimeout(function() {
pdfMake.createPdf(docDefinition).download("Score_Details.pdf");
}, 10000);
}
Issues:
I have that nasty 10 second timeout to allow time for processing, how can I restructure my code to allow the PDF to be made after all canvas data has been performed.
My canvas elements are becoming mixed up when they are converted, the correct order is essential. How can I maintain the order of the DOM elements?
It seems that html2canvas returns a promise:
$scope.pdfMaker = function() {
var promises = [];
var pdfElements = document.getElementsByClassName("pdfElement");
for (var i = 0; i < pdfElements.length; i++) {
promises.push(
html2canvas(pdfElements[i]).then(function(canvas) {
console.log('finished one!');
return {
image: canvas.toDataURL(),
width: 500,
margin: [0, 5]
};
})
);
}
console.log('done calling');
$q.all(promises).then(function(content) {
console.log('all done');
pdfMake.createPdf({ content: content }).download("Score_Details.pdf");
console.log('download one');
}, function(err) {
console.error(err);
})
}
To get your code to work just requires a few mods.
The problem from your description is that the rendering time for html2canvas varies so that they come in an undetermined order. If you make the code inside the for loop a function and pass the index to it, the function will close over the argument variable, you can use that index in the onrendered callback because (that will also close over the index) to place the rendered html in the correct position of the content array.
To know when you have completed all the rendering and can convert to pdf, keep a count of the number of html2canvas renderer you have started. When the onrendered callback is called reduce the count by one. When the count is zero you know all the documents have been rendered so you can then call createPdf.
Below are the changes
$scope.pdfMaker = function() {
var i,pdfElement,content,count,docDefinition;
function makeCanvas(index){ // closure ensures that index is unique inside this function.
count += 1; // count the new render request
html2canvas(pdfElement[index], {
onrendered: function(canvas) { // this function closes over index
content[index] = { // add the canvas content to the correct index
image: canvas.toDataURL(),
width: 500,
margin: [0, 5]
};
count -= 1; // reduce count
if(count === 0){ // is count 0, if so then all done
// render the pdf and download
pdfMake.createPdf(docDefinition).download("Score_Details.pdf");
}
}
});
}
// get elements
pdfElement = document.getElementsByClassName("pdfElement");
content = []; // create content array
count = 0; // set render request counter
docDefinition = {content: content}; //
for (i = pdfElement.length - 1; i >= 0; i--) { //do the loop thing
makeCanvas(i); // call the makeCanvas function with the index
}
}
The only thing I was not sure of was the order you wanted the pdfElements to appear. The changes will place the rendered content into the content array in the same order as document.getElementsByClassName("pdfElement"); gets them.
If you want the documents in the revers order call content.reverse() when the count is back to zero and you are ready to create the pdf.
Closure is a very powerful feature of Javascript. If you are unsure of how closure works then a review is well worth the time.
MDN closures is as good a place as any to start and there are plenty of resources on the net to learn from.
I have developed a 3D viewer of buildings. What I'm trying to add now is the selection of the content of a WMS (Web Map Service) below the building entities.
Basically, I want to be able to select the building at the position were the user left clicks. The colour of the building should change (which works). And I want to retrieve the information of the Web Map Service at the position were the user clicked.
This is what I have coded so far:
var pickColor = Cesium.Color.CYAN.withAlpha(0.7);
var selectedEntity = new Map();
handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
handler.setInputAction(function(click) {
var pickedObject = viewer.scene.pick(click.position);
if (Cesium.defined(pickedObject)) {
var entityId = pickedObject.id._id;
var oldColor = buildingMap.get(entityId).polygon.material.color;
buildingMap.get(entityId).polygon.material.color = pickColor;
selectedEntity.set(entityId, oldColor);
var currentLayer = viewer.scene.imageryLayers.get(1);
if (typeof currentLayer !== 'undefined') {
var info = currentLayer._imageryProvider._tileProvider.getTileCredits(click.position.x, click.position.y, 0);
}
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
However, my variable "info" stays undefined, whereas I expect it to return an array.
For WMS you need to use the wms GetFeatureInfo capability:
var pickRay = viewer.camera.getPickRay(windowPosition);
var featuresPromise = viewer.imageryLayers.pickImageryLayerFeatures(pickRay, viewer.scene);
if (!Cesium.defined(featuresPromise)) {
console.log('No features picked.');
} else {
Cesium.when(featuresPromise, function(features) {
// This function is called asynchronously when the list if picked features is available.
console.log('Number of features: ' + features.length);
if (features.length > 0) {
console.log('First feature name: ' + features[0].name);
}
});
}
What I do right is grab separate columns with single image files and then rescale them. However this time round I would like to grab the array of images and scale the images inside of it then finally store this modified array in the database.
Here is the code:
var Image = require("parse-image");
Pars
e.Cloud.beforeSave("Activity", function(request, response) {
var imagePromises = [];
var activity = request.object;
// you said "image" is always populated, so always add it
imagePromises.push(createImagePromise(activity, "image1", activity.get("image1").url()));
// now conditionally add the other promises, using a loop to further reduce repeated code
for (var i = 2; i < 7; i++) {
var imageColumn = "image" + i;
if (activity.get(imageColumn) && activity.get(imageColumn).url()) {
imagePromises.push(createImagePromise(activity, imageColumn, activity.get(imageColumn).url()));
}
}
// now we have all the promises, wait for them all to finish before we're done
Parse.Promise.when(imagePromises).then(function () {
response.success();
}, function (error) {
response.error(error);
});
function createImagePromise(activity, imageColumn, url) {
// we want to return the promise
return Parse.Cloud.httpRequest({
url: url
}).then(function(response) {
var image = new Image();
return image.setData(response.buffer);
}).then(function(image) {
// Resize the image.
return image.scale({
width: 110,
height: 110
});
}).then(function(image) {
// Make sure it's a JPEG to save disk space and bandwidth.
return image.setFormat("JPEG");
}).then(function(image) {
// Get the image data in a Buffer.
return image.data();
}).then(function(buffer) {
// Save the image into a new file.
var base64 = buffer.toString("base64");
var cropped = new Parse.File("image.jpg", { base64: base64 });
return cropped.save();
}).then(function (cropped) {
// Attach the image file to the original object.
activity.set(imageColumn, cropped);
});
}
});
I have one field in my database "images" it holds an array of images.
Would appreciate some help here.
Thanks for your time
I am working on a HTML5 game and I have a function that should load an image and append it to an array for later displaying to a canvas. The code for the loading function is here...
function loadImage(url) {
box.images.total++;
image = new Image();
image.src = url;
image.onload = function () {
box.images.loaded++;
};
box.images[box.images.length] = image;
}
The code for the holding array is here...
var box = new Object({});
box.images = new Array([]);
And the rendering code is here...
loadImage("images/background.png");
while (box.images.loaded<box.images.total) {console.log("lol");}
box.ctx.drawImage(box.images[0], 0, 0);
My hope was that this would attempt to load the set of images and would increase the counter each time. Then when an image loads it would increase the loaded counter and then once all of the code has run the rest of the code would run. But I get an error saying "function was found that matched the signature provided" and the array appears to contain an empty element.
Also i'm in Chrome on Xubuntu 12.04
Update It turns out that the image was being put in index 1 not 0 and that was why the image wasn't loading. But it still doesn't render the image please help.
Another Update So it turns out that both total and loaded were NaN and so the while loop wasn't able to load at all. I set them to zero and the wile loop didn't terminate and it crashed my browser tab.
You need some sort of Promise or a callback function. For simplicity, I would suggest the callback. Example:
var box = {};
box.images = [];
box.images.total = box.images.loaded = 0;
function loadImage(url, callback) {
box.images.total++;
image = new Image();
image.onload = function () {
box.images.loaded++;
// We call something when the image has loaded
callback();
};
image.src = url;
box.images[box.images.length] = image;
}
function loadAllImages(urls, callback) {
// Loop through each image and load url
for (var i = 0; i < urls.length; ++i) {
var url = urls[i];
loadImage(url, function() {
// When everything loads, call callback
console.log(box.images.total, box.images.loaded);
if (box.images.loaded === box.images.total) callback();
});
}
}
window.onload = function() {
box.ctx = document.querySelector("canvas").getContext("2d");
loadAllImages([
"https://lh4.googleusercontent.com/-rNTjTbBztCY/AAAAAAAAAAI/AAAAAAAAAFM/RNwjnkgQ9eo/photo.jpg?sz=128",
"https://www.gravatar.com/avatar/20e068dd2ad8db5e1403ce6d8ac2ef99?s=128&d=identicon&r=PG&f=1"
], function() {
// Do whatever once everything has loaded
box.ctx.drawImage(box.images[0], 0, 0);
});
};
<canvas>
</canvas>
I'm facing a problem with canvas. No errors is returned. I'm trying to make a loader, that load every ressources like pictures, sounds, videos, etc, before the start of application. The loader must draw the number of ressourses loaded dynamically.
But at this moment, my loader has the result to freeze the browser until it draws the total of ressources loaded.
Tell me if i'm not clear :)
This is the code :
function SimpleLoader(){
var ressources ;
var canvas;
var ctx;
this.getRessources = function(){
return ressources;
};
this.setAllRessources = function(newRessources){
ressources = newRessources;
};
this.getCanvas = function(){
return canvas;
};
this.setCanvas = function(newCanvas){
canvas = newCanvas;
};
this.getCtx = function(){
return ctx;
};
this.setCtx = function(newCtx){
ctx = newCtx;
};
};
SimpleLoader.prototype.init = function (ressources, canvas, ctx){
this.setAllRessources(ressources);
this.setCanvas(canvas);
this.setCtx(ctx);
};
SimpleLoader.prototype.draw = function (){
var that = this;
this.getCtx().clearRect(0, 0, this.getCanvas().width, this.getCanvas().height);
this.getCtx().fillStyle = "black";
this.getCtx().fillRect(0,0,this.getCanvas().width, this.getCanvas().height)
for(var i = 0; i < this.getRessources().length; i++){
var data = this.getRessources()[i];
if(data instanceof Picture){
var drawLoader = function(nbLoad){
that.getCtx().clearRect(0, 0, that.getCanvas().width, that.getCanvas().height);
that.getCtx().fillStyle = "black";
that.getCtx().fillRect(0,0, that.getCanvas().width, that.getCanvas().height);
that.getCtx().fillStyle = "white";
that.getCtx().fillText("Chargement en cours ... " + Number(nbLoad) +"/"+ Number(100), that.getCanvas().width/2, 100 );
}
data.img = new Image();
data.img.src = data.src;
data.img.onload = drawLoader(Number(i)+1); //Update loader to reflect picture loading progress
} else if(data instanceof Animation){
/* Load animation */
} else if(data instanceof Video){
/* Load video */
} else if(data instanceof Sound){
/* Load sound */
}else {
}
}
};
So with this code, all resources are loaded, but i want to display the progress of loading. Some idea of what i missed?
You are "busy-looping" in the loader so the browser doesn't get a chance to redraw/update canvas.
You can implement a setTimeout(getNext, 0) or put the draw function outside polling current status in a requestAnimationFrame loop instead. I would recommend the former in this case.
In pseudo code, this is one way to get it working:
//Global:
currentItem = 0
total = numberOfItems
//The loop:
function getNextItem() {
getItem(currentItem++);
drawProgressToCanvas();
if (currentItem < total)
setTimeout(getNextItem(), 0);
else
isReady();
}
getNextItem(); //start the loader
Adopt as needed.
The setTimeout with a value of 0 will cue up a call next time there is time available (ie. after a redraw, empty event stack etc.). isReady() here is just one way to get to the next step when everything is loaded. (If you notice any problems using 0, try to use for example 16 instead.)
Using requestAnimationFrame is a more low-level and efficient way of doing it. Not all browsers support it at the moment, but there are poly-fills that will get you around that - For this kind of usage it is not so important, but just so you are aware of this option as well (in case you didn't already).