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

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.

Related

amCharts: Map Recentering after JSON DataSource Despite Previous chart.homeGeoPoint

I'm mapping a series of points with amChart; after loading the data from an external JSON source, the map re-centers instead of staying at the point I'd set earlier with chart.homeGeoPoint.
I believe I need to use an event listener and set the homeGeoPoint after the map renders the points... but I'm at a bit of a loss; the only events I've found are from dataSource.events, and those appear to be related to fetching/parsing the JSON, as opposed to rendering the map.
// Create map instance
var chart = am4core.create("chartdiv", am4maps.MapChart);
// Set map definition
chart.geodata = am4geodata_region_world_northAmericaLow;
// Set projection
chart.projection = new am4maps.projections.Miller();
// Initial Position / Zoom
chart.homeZoomLevel = 2.6;
chart.homeGeoPoint = {
latitude: 39,
longitude: -96.2456
};
// Series for World map
var worldSeries = chart.series.push(new am4maps.MapPolygonSeries());
worldSeries.useGeodata = true;
// Markers
// Create image series
var imageSeries = chart.series.push(new am4maps.MapImageSeries());
// Create a circle image in image series template so it gets replicated to all new images
var imageSeriesTemplate = imageSeries.mapImages.template;
var circle = imageSeriesTemplate.createChild(am4core.Circle);
circle.radius = 5;
circle.fill = am4core.color("#116ad6");
circle.stroke = am4core.color("#FFFFFF");
circle.strokeWidth = 2;
circle.nonScaling = true;
circle.tooltipText = "{title}";
// Set property fields
imageSeriesTemplate.propertyFields.latitude = "latitude";
imageSeriesTemplate.propertyFields.longitude = "longitude";
imageSeriesTemplate.propertyFields.url = "url";
// Load data
imageSeries.dataSource.url = "/foo/map-points.php";
imageSeries.dataSource.parser = new am4core.JSONParser();
imageSeries.dataSource.parser.options.emptyAs = 0;
// Center after render
imageSeries.dataSource.events.on("done", function(ev) {
// This doesn't work - perhaps it is firing too early?
chart.homeGeoPoint = {
latitude: 39,
longitude: -96.2456
};
});
By request, here is a foo.json file for expirmenting with.
[{"title":"ISP","url":"\/airport\/kisp\/","latitude":40.7952,"longitude":-73.1002},{"title":"AEX","url":"\/airport\/kaex\/","latitude":31.3274,"longitude":-92.5486}]
What do I need to do to make sure the map stays centered on my desired location after the JSON data are loaded and rendered?
I've created an issue on GitHub in regards to why the map re-positions on the MapImageSeries' dataSource load and how to better work with that. (If you've a GitHub account, please subscribe to the issue.)
In the meantime, presuming the first time your dataSource gets its data that the user hasn't moved the map and we want to maintain homeGeoPoint as the current center, we can chain events to achieve that.
When the dataSource is "done" with its data, that doesn't necessarily imply anything has been done on the actual map level. The data still needs to propagate to the MapImageSeries, that still needs to create MapImages per data item, have the data validated/parsed there, and for whatever reason the map position shifts around. So the first time that happens (using events.once instead of events.on), we then listen for the MapImageSeries' "datavalidated" event also only one time (because "datavalidated" will have run before this, e.g. as soon as you create the MapImageSeries, if no data is supplied or it's taking some time, it will still run the event and the "inited" event, i.e. I guess you can say the series itself will successfully render nothing).
And to center the map we use chart.goHome(0);, this method will zoom to your homeGeoPoint and homeZoomLevel, the 0 is for how long the animation duration should run, i.e. just do the work, don't animate.
So all that together will look something like this:
// Center after render
imageSeries.dataSource.events.once("done", function(ev) {
imageSeries.events.once("datavalidated", function() {
chart.goHome(0);
});
});
Here's a demo:
https://codepen.io/team/amcharts/pen/239bfdc8689c65468df32d71b29759b8
Even though the map does re-position once the MapImageSeries loads, then it re-centers with the above code, I haven't actually seen the map shift at all anymore. So it looks to me the above code is doing the job of maintaining the homeGeoPoint. Let me know if that is still the case once implemented in your application.

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

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();
};

fineuploader - Read file dimensions / Validate by resolution

I would like to validate by file dimensions (resolution).
on the documentation page there is only information regarding file name and size, nothing at all in the docs about dimensions, and I also had no luck on Google.
The purpose of this is that I don't want users to upload low-res photos to my server. Thanks.
As Ray Nicholus had suggested, using the getFile method to get the File object and then use that with the internal instance object qq.ImageValidation to run fineuploader's validation on the file. A promise must be return because this proccess is async.
function onSubmit(e, id, filename){
var promise = validateByDimensions(id, [1024, 600]);
return promise;
}
function validateByDimensions(id, dimensionsArr){
var deferred = new $.Deferred(),
file = uploaderElm.fineUploader('getFile', id),
imageValidator = new qq.ImageValidation(file, function(){}),
result = imageValidator.validate({
minWidth : dimensionsArr[0],
minHeight : dimensionsArr[1]
});
result.done(function(status){
if( status )
deferred.reject();
else
deferred.resolve();
});
return deferred.promise();
}
Remained question:
Now I wonder how to show the thumbnail of the image that was rejected, while not uploading it to the server, the UI could mark in a different color as an "invalid image", yet the user could see which images we valid and which weren't...
- Update - (regarding the question above)
While I do not see how I could have the default behavior of a thumbnail added to the uploader, but not being uploaded, but there is a way to generate thumbnail manually, like so:
var img = new Image();
uploaderElm.fineUploader("drawThumbnail", id, img, 200, false);
but then I'll to create an item to be inserted to qq-upload-list myself, and handle it all myself..but still it's not so hard.
Update (get even more control over dimensions validation)
You will have to edit (currently) the qq.ImageValidation function to expose outside the private function getWidthHeight. just change that function deceleration to:
this.getWidthHeight = function(){
Also, it would be even better to change the this.validate function to:
this.validate = function(limits) {
var validationEffort = new qq.Promise();
log("Attempting to validate image.");
if (hasNonZeroLimits(limits)) {
this.getWidthHeight().done(function(dimensions){
var failingLimit = getFailingLimit(limits, dimensions);
if (failingLimit) {
validationEffort.failure({ fail:failingLimit, dimensions:dimensions });
}
else {
validationEffort.success({ dimensions:dimensions });
}
}, validationEffort.success);
}
else {
validationEffort.success();
}
return validationEffort;
};
So you would get the fail reason, as well as the dimensions. always nice to have more control.
Now, we could write the custom validation like this:
function validateFileDimensions(dimensionsLimits){
var deferred = new $.Deferred(),
file = this.holderElm.fineUploader('getFile', id),
imageValidator = new qq.ImageValidation(file, function(){});
imageValidator.getWidthHeight().done(function(dimensions){
var minWidth = dimensions.width > dimensionsLimits.width,
minHeight = dimensions.height > dimensionsLimits.height;
// if min-width or min-height satisfied the limits, then approve the image
if( minWidth || minHeight )
deferred.resolve();
else
deferred.reject();
});
return deferred.promise();
}
This approach gives much more flexibility. For example, you would want to have different validation for portrait images than landscape ones, you could easily identify the image orientation and run your own custom code to do whatever.

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 ;)

How to delete markers from google map before painting new ones on idle event

I'm drawing markers on a google map after it's been panned or zoomed.
If the viewport has not totally changed than the markers that appeared before the move and should still appear after it are being painted on top of themselves again and again.
I know I have to delete all the markers from the map before the new markers are painted and I know I should be using marker.setMap(null);.
I just don't know how and where to fit it in my code.
google.maps.event.addListener(map, 'idle', showMarkers);
function showMarkers(){
// get viewport bounds
var southWestLat = map.getBounds().getSouthWest().lat();
var southWestLng = map.getBounds().getSouthWest().lng();
var northEastLat = map.getBounds().getNorthEast().lat();
var northEastLng = map.getBounds().getNorthEast().lng();
var marker;
$.ajax({
type: "POST",
url: "markers.php",
dataType: "json",
data: ({'southWestLat' : southWestLat , 'southWestLng' : southWestLng , 'northEastLat' : northEastLat , 'northEastLng' : northEastLng}),
success: function(coordinatesMap){
for (var id in coordinatesMap){
if (coordinatesMap.hasOwnProperty(id)){
marker = new google.maps.Marker({
position: new google.maps.LatLng(coordinatesMap[id].lat,coordinatesMap[id].lng),
map: map
});
}
}
}
});
}
For performance reasoning you don't want to clear all the markers; Only the ones no longer on the map. This can be accomplished by storing the marker in an array or object. On 'success' you want to check if a marker is already at the lat lng using your array/object. If so then don't add a new one, if not then add it. After you have finished processing the markers in the ajax request, remove the ones no longer visible on the map.

Resources