OpenLayers 3: Scaling canvas with text with 125% DPI since v4.x - html5-canvas

I've created two identical fiddles with different OpenLayers-Versions:
OpenLayers v3.18.0 and OpenLayers 4.1.1
The objective is to export a PNG in high resolution. I didn't include the actual exporting of the file. It is explained here if interested.
It all worked fine up to the newer version (I think until a 4.x version).
If you have the DPI-Setting in windows on 100%, both fiddles do the same - but if you change your DPI-Setting to 125%, the latter fiddle does not update the text Some text! and it becomes really small and is located in the wrong place.
The map stays like that, until I click into it (or I call map.updateSize()). So I thought, I add map.updateSize() at the end of precompose - but no matter where I do it, the exported image is wrong as the updateSize() seems to be async and happening AFTER postcompose.
I didn't find a breaking change regarding this issue. Am I overlooking something or is it a bug? Any suggestion for a workaround?

Thanks to the issue I opened on github I came up with the following solution. The most interesting part is the creation of a second ol.Map with a desired pixelRatio:
saveToFile = function (fileName, opt_ChangeSize, opt_PixelRatio, opt_DelayRenderPromise) {
var newMapComponent,
originalSize = mapComponent.getSize();
opt_ChangeSize = opt_ChangeSize || { width: originalSize[0], height: originalSize[1] };
var div = $(document.createElement("div"));
div.attr('id', 'DIV_SaveToFile_Renderer_' + (new Date()).getTime());
div.css('position', 'absolute');
div.css('top', '0');
div.css('left', '0');
div.css('visibility', 'hidden');
div.css('width', opt_ChangeSize.width + 'px');
div.css('height', opt_ChangeSize.height + 'px');
$('body').append(div);
newMapComponent = new ol.Map({
target: div[0].id,
layers: mapComponent.getLayers(),
pixelRatio: opt_PixelRatio,
view: mapComponent.getView()
});
// opt_DelayRenderPromise in this case returns when loading of some features has completed. It could also be postrender of map or whatever.
$q.when(opt_DelayRenderPromise).then(function () {
$timeout(function () {
var data,
canvas = div.find('canvas')[0];
data = canvas.toDataURL('image/png');
data = data.replace(/^data:image\/(png);base64,/, "");
MyUtilities.SaveBlobFromBase64(data, fileName);
div.remove();
mapComponent.setSize(originalSize);
mapComponent.renderSync();
});
});
mapComponent.setSize([opt_ChangeSize.width, opt_ChangeSize.height]);
mapComponent.renderSync();
};

Related

Using L.esri.DynamicMapLayer, is it possible to bind a mouseover event rather than a pop-up on a dynamic map?

I'm aware of binding a pop-up to ESRI's L.esri.DynamicMapLayer here. The following code below is successful.
$.ajax({
type: 'GET',
url: url + '?f=json',
data: { layer: fooType },
dataType: 'json',
success: function(json) {
var foo_layer = fooLayers[fooType].layers;
foo = L.esri.dynamicMapLayer({
url: url,
layers: [foo_layer],
transparent: true
}).addTo(map).bringToFront();
foo.bindPopup(function(error, featureCollection) {
if (error || featureCollection.features.length === 0) {
return false;
} else {
var obj = featureCollection.features[0].properties;
var val = obj['Pixel Value'];
var lat = featureCollection.features[0].geometry.coordinates[1];
var lon = featureCollection.features[0].geometry.coordinates[0];
new L.responsivePopup({
autoPanPadding: [10, 10],
closeButton: true,
autoPan: false
}).setContent(parseFloat(val).toFixed(2)).setLatLng([lat, lon]).openOn(map);
}
});
}
});
But rather than a click response I am wondering as to whether you can mouseover using bindTooltip instead on a dynamic map. I've looked at the documentation for L.esri.DynamicMapLayer which says it is an extension of L.ImageOverlay. But perhaps there is an issue outlined here that I'm not fully understanding. Maybe it is not even related.
Aside, I've been testing multiple variations of even the simplest code to get things to work below but have been unsuccessful. Perhaps because this is asynchronous behavior it isn't possible. Looking for any guidance and/or explanation(s). Very novice programmer and much obliged for expertise.
$.ajax({
type: 'GET',
url: url + '?f=json',
data: { layer: fooType },
dataType: 'json',
success: function(json) {
var foo_layer = fooLayers[fooType].layers;
foo = L.esri.dynamicMapLayer({
url: url,
layers: [foo_layer],
transparent: true
}).addTo(map).bringToFront();
foo.bindTooltip(function(error, featureCollection) {
if (error || featureCollection.features.length === 0) {
return false;
} else {
new L.tooltip({
sticky: true
}).setContent('blah').setLatLng([lat,lng]).openOn(map);
}
});
}
});
Serendipitously, I have been working on a different problem, and one of the byproducts of that problem may come in handy for you.
Your primary issue is the asynchronous nature of the click event. If you open up your map (the first jsfiddle in your comment), open your dev tools network tab, and start clicking around, you will see a new network request made for every click. That's how a lot of esri query functions work - they need to query the server and check the database for the value you want at the given latlng. If you tried to attach that same behavior to a mousemove event, you'll trigger a huge number of network requests and you'll overload the browser - bad news.
One solution of what you can do, and its a lot more work, is to read the pixel data under the cursor of the image returned from the esri image service. If you know the exact rgb value of the pixel under the cursor, and you know what value that rgb value corresponds to in the map legend, you can achieve your result.
Here is a working example
And Here is the codesandbox source code. Don't be afraid to hit refresh, CSB is little wonky in the way it transpiles the modules.
What is happening here? Let's look step by step:
On map events like load, zoomend, moveend, a specialized function is fetching the same image that L.esri.dynamicMapLayer does, using something called EsriImageRequest, which is a class I wrote that reuses a lot of esri-leaflet's internal logic:
map.on("load moveend zoomend resize", applyImage);
const flashFloodImageRequest = new EsriImageRequest({
url: layer_url,
f: "image",
sublayer: "3",
});
function applyImage() {
flashFloodImageRequest
.fetchImage([map.getBounds()], map.getZoom())
.then((image) => {
//do something with the image
});
}
An instance of EsriImageRequest has the fetchImage method, which takes an array of L.LatLngBounds and a map zoom level, and returns an image - the same image that your dynamicMapLayer displays on the map.
EsriImageRequest is probably extra code that you don't need, but I happen to have just run into this issue. I wrote this because my app runs on a nodejs server, and I don't have a map instance with an L.esri.dynamicMapLayer. As a simpler alternative, you can target the leaflet DOM <img> element that shows your dynamicMapLayer, use that as your image source that we'll need in step 2. You will have to set up a listener on the src attribute of that element, and run the applyImage in that listener. If you're not familiar with how leaflet manages the DOM, look into your elements tab in the inspector, and you can find the <img> element here:
I'd recommend doing it that way, and not the way my example shows. Like I said, I happened to have just been working on a sort-of related issue.
Earlier in the code, I had set up a canvas, and using the css position, pointer-events, and opacity properties, it lays exactly over the map, but is set to take no interaction (I gave it a small amount of opacity in the example, but you'd probably want to set opacity to 0). In the applyImage function, the image we got is written to that canvas:
// earlier...
const mapContainer = document.getElementById("leafletMapid");
const canvas = document.getElementById("mycanvas");
const height = mapContainer.getBoundingClientRect().height;
const width = mapContainer.getBoundingClientRect().width;
canvas.height = height;
canvas.width = width;
const ctx = canvas.getContext("2d");
// inside applyImage .then:
.then((image) => {
image.crossOrigin = "*";
ctx.drawImage(image, 0, 0, width, height);
});
Now we have an invisible canvas who's pixel content is exactly the same as the dynamicMapLayer's.
Now we can listen to the map's mousemove event, and get the mouse's rgba pixel value from the canvas we created. If you read into my other question, you can see how I got the array of legend values, and how I'm using that array to map the pixel's rgba value back to the legend's value for that color. We can use the legend's value for that pixel, and set the popup content to that value.
map.on("mousemove", (e) => {
// get xy position on cavnas of the latlng
const { x, y } = map.latLngToContainerPoint(e.latlng);
// get the pixeldata for that xy position
const pixelData = ctx.getImageData(x, y, 1, 1);
const [R, G, B, A] = pixelData.data;
const rgbvalue = { R, G, B, A };
// get the value of that pixel according to the layer's legend
const value = legend.find((symbol) =>
compareObjectWithTolerance(symbol.rgbvalue, rgbvalue, 5)
);
// open the popup if its not already open
if (!popup.isOpen()) {
popup.setLatLng(e.latlng);
popup.openOn(map);
}
// set the position of the popup to the mouse cursor
popup.setLatLng(e.latlng);
// set the value of the popup content to the value you got from the legend
popup.setContent(`Value: ${value?.label || "unknown"}`);
});
As you can see, I'm also setting the latlng of the popup to wherever the mouse is. With closeButton: false in the popup options, it behaves much like a tooltip. I tried getting it to work with a proper L.tooltip, but I was having some trouble myself. This seems to create the same effect.
Sorry if this was a long answer. There are many ways to adapt / improve my code sample, but this should get you started.

Kendo Grid: Removing dirty cell indicators

I have been looking at a way to save off my client side edited grid data automatically when the user changes to another row (just like in access, sql management studio etc). It really seems to be a bit of a challenge to do.
One scheme was to use the data source sync, but this ha the problem of loosing our cell position (it always jumped to cell 0, 0).
I have been shown some clever work arounds (go back to the cell after the case, which by the way is hugely appreciated thanks),
but it after some lengthy testing (by myself and others) seemed to be a little "glitchy" (perhaps I just need to work on this more)
At any rate, I wanted to explore perhaps not using this datasource sync and perhaps just do the server side calls "manually" (which is a bit is a pity, but if that's what we need to do, so be it). If I do this, I would want to reset the cell little red cell "dirty" indicators.
I thought I could use something similar to this scheme (except rather than resetting the flag, I want to unset).
So, as in the above link, I have the following..
var pendingChanges = [];
function gridEdit(e) {
var cellHeader = $("#gridID").find("th[data-field='" + e.field + "']");
if (cellHeader[0] != undefined) {
var pendingChange = new Object();
pendingChange.PropertyName = e.field;
pendingChange.ColumnIndex = cellHeader[0].cellIndex;
pendingChange.uid = e.items[0].uid;
pendingChanges.push(pendingChange);
}
}
where we call gridEdit from the datasource change..
var dataSrc = new kendo.data.DataSource({
change: function (e) {
gridEdit(e);
},
Now assuming we have a callback that detects the row change, I thought I could do the following...
// clear cell property (red indicator)
for (var i = 0; i < pendingChanges.length; i++) {
var row = grid.tbody.find("tr[data-uid='" + pendingChanges[i].uid + "']");
var cell = row.find("td:eq(" + pendingChanges[i].ColumnIndex + ")");
if (cell.hasClass("k-dirty-cell")) {
cell.removeClass("k-dirty-cell");
console.log("removed dirty class");
}
}
pendingChanges.length = 0;
// No good, we loose current cell again! (sigh..)
//grid.refresh();
When this didn't work, I also tried resetting the data source dirty flag..
// clear dirty flag from the database
var dirtyRows = $.grep(vm.gridData.view(),
function (item) {
return item.dirty == true;
})
if (dirtyRows && dirtyRows.length > 0) {
dirtyRows[0].dirty = false;
}
demo here
After none of the above worked, I tried the grid.refresh(), but this has the same problem as the datasource sync (we loose our current cell)
Would anyone have any idea how I can clear this cell indicator, without refreshing the whole grid that seems to totally loose our editing context?
Thanks in advance for any help!
Css :
.k-dirty-clear {
border-width:0;
}
Grid edit event :
edit: function(e) {
$("#grid .k-dirty").addClass("k-dirty-clear"); //Clear indicators
$("#grid .k-dirty").removeClass("k-dirty-clear"); //Show indicators
}
http://jsbin.com/celajewuwe/2/edit
Simple solution for resolve that problem is to override the color of the "flag" to transparent.
just override the ".k-dirty" class (border-color)
just adding the above lines to your css
CSS:
//k-dirty is the class that kendo grid use for mark edited cells that not saved yet.
//we override that class cause we do not want the red flag
.k-dirty {
border-color:transparent transparent transparent transparent;
}
This can also be done by applying the below style,
<style>
.k-dirty{
display: none;
}
</style>

How do I make iScroll5 work when the image is generated from a DB?

I am using iScroll5 in a PhoneGap project. On the index page, user will click on a series of thumbnails generated from a database, then the image ID chosen will be written to localstorage, the page will change, the image ID will be pulled from localstorage and the image displayed.
It works fine if I reference the image directly (not from the DB) this way (as a test):
<body onload="loaded()">
<div id='wrapper'><div id='scroller'>
<ul><li><a id='output' href='index.html' onclick='returnTo()'></a></li></ul>
</div></div>
<script>
var newWP = document.createElement('img');
newWP.src = '0buggies/0118_buggies/wallpaper-18b2.jpg';
document.getElementById('output').appendChild(newWP);
</script>
</body>
I can pinch/zoom to resize the image for the screen (the main function my app requires), and scroll the image on the X and Y axis, then upon tapping the image, I will be returned to the index page. All of this works.
But if I pull the image out of a database and reference it the following way, all other aspects of the page code being the same, pinch/zoom does not work, though the picture is displayed and I can scroll on X and Y:
// ... DB code here ...
function querySuccess(tx, results) {
var path = results.rows.item.category +
"/" + results.rows.item.subcat +
"/" + results.rows.item.filename_lg;
document.getElementById("output").innerHTML = "<img src='" + path +
"'>";
}
// ... more DB code here ...
<body onload="loaded()">
<div id='wrapper'> <ul><li><a id='output' href='index.html'
onclick='returnTo()'></a></li></ul> </div>
How do I make iScroll5 work when the image is generated from a DB? I'm using the same CSS and iScroll JS on both pages. (iScroll4 has the same problem as iScroll 5 above.) I am using the SQLite DB plugin (from http://iphonedevlog.wordpress.com/2014/04/07/installing-chris-brodys-sqlite-database-with-cordova-cli-android/ which is my own site).
Try calling refresh on the scrollbar to get it to recognize the DOM change.
Best to wrap it in a 0-delay setTimeout, like so (Stolen from http://iscrolljs.com/#refresh)
:
setTimeout(function () {
myScroll.refresh();
}, 0);
If it takes time for the image to load, you'll want to wait until it's loaded entirely, unless you know the dimensions up-front.
When dealing with images loaded dynamically things get a little more complicated. The reason is that the image dimensions are known to the browser only when the image itself has been fully loaded (and not when the img tag has been added to the DOM).
Your best bet is to explicitly declare the image width/height. You'd do this like so:
function querySuccess (results) {
var path = results.rows.item.category +
"/" + results.rows.item.subcat +
"/" + results.rows.item.filename_lg;
var img = document.createElement('img');
img.width = 100;
img.height = 100;
img.src = path;
document.getElementById('output').appendChild(img);
// need to refresh iscroll in case the previous img was smaller/bigger than the new one
iScrollInstance.refresh();
}
If width/height are unknown you could save the image dimensions into the database and retrieve them together with the image path.
function querySuccess (results) {
var path = results.rows.item.category +
"/" + results.rows.item.subcat +
"/" + results.rows.item.filename_lg;
var img = document.createElement('img');
img.width = results.width;
img.height = results.height;
img.src = path;
document.getElementById('output').appendChild(img);
// need to refresh iscroll in case the previous img was smaller/bigger than the new one
iScrollInstance.refresh();
}
If you can't evaluate the image dimensions in any way then you have to wait for the image to be fully loaded and at that point you can perform an iScroll.refresh(). Something like this:
function querySuccess (results) {
var path = results.rows.item.category +
"/" + results.rows.item.subcat +
"/" + results.rows.item.filename_lg;
var img = document.createElement('img');
img.onload = function () {
setTimeout(iScrollInstance.refresh.bind(iScrollInstance), 10); // give 10ms rest
}
img.onerror = function () {
// you may want to deal with error404 or connection errors
}
img.src = path;
document.getElementById('output').appendChild(img);
}
Why is the viewport user-scalable prop different on each sample? works=no, broken=yes
Just an observation.
fwiw, here are a few things to look into:
Uncomment the deviceReady addListener, as Cordova init really depends on this.
Your loaded() method assigns myScroll a new iScroll, then explicitly calls onDeviceReady(), which then declares var myScroll; -- this seems inherently problematic - rework this.
If 1 & 2 don't help, then I suggest moving queryDB(tx); from populateDB() to successCB() and commenting out the myScroll.refresh()
And just a note, I find that logging to console is less intrusive than using alerts when trying to track down a symptom that seems to be messing with events firing, or timing concerns.

KineticJS : get image array id

Here is the problem :
I have a canvas, and four (would be more in future, but 4 for testing...anyway, doesn't matter) images that can be "poped" into the canvas by clicking on it.
Each image can be present multiple times in the canvas.
So far, poping is working fine, images are draggable... But I can't add some resize or zIndex function as I can only select the last image add to the canvas.
In a ideal world, I would like, by clicking/dragging an image, put it on top of the canvas, and kinda "select" it, so that I can connect the resize functions to the image.
But with the array of images, I can't manage to identify properly the item dragged, and can't use (or don't manage to use) the selectors.
Thank you.
EDIT : some code
var imgCpt = 0;
var image = [];
function addDetails(img) {
imgCpt++;
var imageObj = new Image();
imageObj.onload = function() {
image[imgCpt] = new Kinetic.Image({
x: 0,
y: 0,
image: imageObj,
draggable: true,
id:image[imgCpt]
});
image[imgCpt].setX((stage.getWidth()/2) - (image[imgCpt].getWidth()/2));
image[imgCpt].setY((stage.getHeight()/2) - (image[imgCpt].getHeight()/2));
eval(image[imgCpt]).on('click', function() {
alert(eval(imgCpt));
});
layer.add(image[imgCpt]);
stage.add(layer);
};
imageObj.src = 'uploads/'+img;
}
I've already tried different solutions : multiple layer, and acting on it instead of acting on image, working with shapes filled with image instead of image, but it's always the same problem : I can't get the id of the concerned element (instead of the id of the last insert element)
This version works with array, but I tried yersterday to build the image id with eval(); without more success.
Thank you for your help
EDIT² : sorry to insist, but I would really be glad to have some assistance on this point, even if I think it's more JS related than pure KineticJS related.
Thank you.
Ok Guys, just solved the problem :
eval("image["+imgCpt+"].on('click', function() {alert("+imgCpt+");});");
Instead of :
eval(image[imgCpt]).on('click', function() {
alert(eval(imgCpt));
});
Now time to set a true action behind the click.
Thank you for helping ;)

jQuery — a .live() > each() >.load.() finella

EDIT - URL to see the issue http://syndex.me
I am dynamically resizing images bigger than the browser to equal the size of the browser.
This was no easy feat as we had to wait for the images to load first in order to check first if the image was bigger than the window.
We got to this stage (which works):
var maxxxHeight = $(window).height();
$(".theImage").children('img').each(function() {
$(this).load( function() { // only if images can be loaded dynamically
handleImageLoad(this);
});
handleImageLoad(this);
});
function handleImageLoad(img)
{
var $img = $(img), // declare local and cache jQuery for the argument
myHeight = $img.height();
if ( myHeight > maxxxHeight ){
$img.height(maxxxHeight);
$img.next().text("Browser " + maxxxHeight + " image height " + myHeight);
};
}
The thing is, the page is an infinite scroll (I'm using this)
I know that you are not able to attach 'live' to 'each' as 'live' deals with events, and 'each' is not an event.
I've looked at things like the livequery plugin and using the ajaxComplete function.
With livequery i changed
$(".theImage").children('img').each(function() {
to
$(".theImage").children('img').livequery(function(){
But that didnt work.
ajaxComplete seemed to do nothing so i'm guessing the inifinte scroll i'm using is not ajax based. (surely it is though?)
Thanks
Use delegate:
$(".theImage").delegate('img', function() {
$(this).load( function() { // only if images can be loaded dynamically
handleImageLoad(this);
});
handleImageLoad(this);
});
The problem is that your infinite scroll plugin does not provide the callback functionality. Once your pictures are loaded there is no way to affect them.
I have tried to modify your plugin, so that it will serve your needs, please see http://jsfiddle.net/R8yLZ/
Scroll down the JS section till you see a bunch of comments.
This looks really complicated, and I probably don't get it at all, but I'll try anyway :-)
$("img", ".theImage").bind("load", function() {
var winH = $(window).height();
var imgH = $(this).height();
if (winH < imgH) {
$(this).height(winH);
$(this).next().text("Browser " + winH + " image height " + imgH);
}
});

Resources