jsPlumb: Large endpoint to make touch easier - jsplumb

My implementation has two lists of elements and you basically connect items from the list on the left to items from the list on the right.
We have set the endpoints to be transparent so on the desktop it looks like you just drag from elements on one side to the other.
It works brilliantly on desktop but on touch devices we need the endpoint to cover exactly the space of the element it is attached to in order for it to work as the user would expect. So in effect the whole element becomes an endpoint.
I can achieve this by making the endpoint a rectangle of the same size and anchoring in the center, but I want to anchor on the right/left. If I anchor in the right/left, then my large endpoint is then half covering the element.
Is there a way of anchoring on the right/left but have the endpoint covering the element entirely? Or offsetting the endpoint somehow so it still covers the element?
Thanks in advance for any help
Btw jsPlumb is awesome

You can do it using a transparent image. I did it that way.
endpoint:[ "Image", { src:"/javascript/project/img/c.png",
cssClass:'node' } ]

If you use jsPlumb.addEndpoint() you can add additional parameters for each endpoint you create.
I got this code from one of the examples:
var exampleGreyEndpointOptions = {
endpoint:"Rectangle",
paintStyle:{ width:25, height:21, fillStyle: color }, // size and color of the endpoint
isSource:true,
connectorStyle : { strokeStyle: color }, // color of the line AND the arrow
isTarget:false,
maxConnections:20,
anchor: "BottomCenter"
};
var endpointOptions = {
isTarget:true,
endpoint:"Dot",
paintStyle:{
fillStyle: color // color of the end point
},
beforeDrop: function(info){
return handleConnectionDrop(info);
},
dropOptions: exampleDropOptions,
maxConnections: 20,
anchor: "TopCenter"
};
function InitContainer(el, data) {
elem = $(el);
jsPlumb.addEndpoint(el, {
uuid: elem.attr("id")+"sr"
}, exampleGreyEndpointOptions);
jsPlumb.addEndpoint(el, {
uuid: elem.attr("id")+"tr",
parameters: data // store data in the drain Endpoint
}, endpointOptions);
....
}
in this function I added two endpoints to one element, each with different attributes.
The actual problem you have is that the surface of the endpoint needs to be offset from it's position so that it is within the element. This is mentioned in the API docs, but I didn't find an example yet:
http://jsplumb.org/apidocs/files/jquery-jsPlumb-1-3-16-all-js.html#Endpoint.paint
You can change the position of a anchor, not only by the documented text (like "TopCenter") But also by a array:
anchor: [0.2, 1,0,1]
Give it a try, You'll be able to position the anchor relative to it's element

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.

relevant hexids inside a hexagon

i am generating a set of hex ids via "h3.geoToH3(lng, lat, res)".
after that i am creating a FeatureCollection via "let polygons = geojson2h3.h3SetToFeatureCollection(hexIds)"
When i am clicking on a hexagon on the map i just want the ids of the relevant hexagons inside the initiated hexagon.
How can i do that in a performant way?
getHexIds = (res) => {
let hexIds = this.props.stations.features.map(element => {
const [lng, lat] = element.geometry.coordinates;
const hexId = h3.geoToH3(lng, lat, res);
return hexId;
})
let polygons = geojson2h3.h3SetToFeatureCollection(hexIds);
this.setState({
polygons: polygons
}, () => {
this.geoJsonLayer.current.leafletElement.clearLayers().addData(this.state.polygons);
})
// console.info(this.state.polygons, res);
}
onEachFeature = (feature, layer) => {
layer.on('click', (e) => {
// show the relevant hexids
})
}
If I understand the question correctly, this might have more to do with your map library (presumably Leaflet) than H3, but here are some general approaches:
If you want to get the H3 index at the point where the user clicked, find the lat/long (probably something your map library provides in the click event) and then just run it through h3.geoToH3(lat, long) again. This is very fast, and it's much easier to recalculate it than to store it and look it up.
If you want to get a set of H3 indexes associated with the click point (i.e. you have multiple features, each made up of multiple H3 indexes), then you usually want to build a reverse index like {[h3Index]: h3Set}. Then you can use h3.geoToH3 to find the index at the click point, and look that up in your reverse index to find the set.
If your map doesn't give you lat/long on click, you can use a library like viewport-mercator-project to find the lat/long from the x/y screen coordinates of the click (available on the DOM event).

jsplumb: Creating two connections from the same source anchor

I'm having a hell of a time with jsPlumb trying to create two connections from the same source anchor.
For example, in the JSFiddle below I'm trying to create two connections from the block 1 anchor to the two other blocks 2 and 3.
http://jsfiddle.net/dutchman71/TYerW/3/
For some reason it works fine in the jsPlumb example here with the green dot anchors.
http://jsplumb.org/jquery/draggableConnectorsDemo.html#
Can anyone tell my what I'm missing?
var endpointOptions = {
anchor:"BottomCenter",
maxConnections:-1,
isSource:true,
isTarget:true,
endpoint:["Dot", {radius:6}],
setDragAllowedWhenFull:true,
paintStyle:{fillStyle:"#5b9ada"},
connectorStyle : { lineWidth: 4, strokeStyle:"#5b9ada" },
connector:[ "Bezier", { curviness:1 }],
connectorOverlays:[
[ "Arrow", { width:15, length:15, location:1, id:"arrow" } ],
[ "Label", { label:"", id:"label" } ]
]
}
jsPlumb.bind("ready", function() {
jsPlumb.addEndpoint('block1', endpointOptions);
jsPlumb.addEndpoint('block2', endpointOptions);
jsPlumb.addEndpoint('block3', endpointOptions);
jsPlumb.draggable('block1');
jsPlumb.draggable('block2');
jsPlumb.draggable('block3');
});
A nice guy on Google groups helped me out on this one: the jsPlumb version I included from another sample is obsolete. If I include this one http://jsplumb.org/js/jquery.jsPlumb-1.3.16-all-min.js is works fine.
set maxconnections to a positive integer. that should do it.
and maybe you should make your endpoints bigger and give the connector lines a smallr z-index, than the enpoints, so you won't missclick on the canvas instead of the div
(the canvas blocks almost the whole div on Block 1 after a connection is established to Block 2 check it in your element inspector/firebug/etc)
Change the radius of the endPoint to endpoint
["Dot", {radius:1}],
and connectorStyle's line width to 1
connectorStyle : { lineWidth: 4, strokeStyle:"#5b9ada" }

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

highcharts mark points after graph is drawn

I am using highcharts to graph multilple series (several lines with multiple points each on one chart). The user selects one or more points on multiple lines. Data about the selected points is shown in a gridview on my asp page. After some server side logic I would like to redraw the page and put an image, marker, flag or some other way of showing the user the redrawn graph with those points "marked".
I have been playing with jquery to add an image (small circle) to the div where the chart is rendered but not having much luck with the X/Y position of the image within the div.
Any advice or examples on how I might do this? Not married to image in DIV other suggestions appreciated.
I figured it out. I made a function that is called when the point is clicked passing the whole point object. An if statement toggles the marker of the ponit and using the acumulate = true it shows all the points on my curve that have been selected. Likewise if it is already selected it toggles the marker off. Much easier than what I was trying.
Here is my function to toggle point and make them all seleted
function ChartClicked(oPointObject) {
if (oPointObject.selected) {
oPointObject.select(false, true);
}
else {
oPointObject.select(true, true);
}
}
Here is a snipet of my graph. It is in the plotOptions I call the click event
plotOptions: {
series: {
cursor: 'pointer',
point: {
events: {
click: function () {
ChartClicked(this);
}
}
}
}
},
Hope this helps someone else.

Resources