Leaflet / Mapbox - Drag & Drop - drag-and-drop

I have a web based map that is using Mapbox / Leaflet JS API.
On the map, I have several stationary markers and other markers that I am moving around based on GPS data that is pushed to the browser. When a moving marker is dropped on a stationary marker, I want to identify the two markers that were involved.
I have implemented a handler for the moving marker's "dragend" event which enables me to identify the marker that was dragged/dropped.
My questions is, how can I identify the marker that it was dropped on?

That's quite hard to do, because the only thing that lets you correctly identify a marker is it's latitude/longitude position. So if you try to drop a marker onto a marker with lat/lng 0,0, you need to drop it exactly onto that position which will turn out to be a very hard thing to do.
You could ofcourse build some sort of tolerance into it, but that tolerance will need to vary according to zoom level which i think will be very hard to get right. You could do something like this:
// Drag has ended
marker.on('dragend', function (e) {
// Get position of dropped marker
var latLng = e.target.getLatLng();
// Object to hold nearest marker and distance
var nearest = {};
// Loop over layer which holds rest of the markers
featureLayer.eachLayer(function(layer) {
// Calculate distance between each marker and dropped marker
var distance = latLng.distanceTo(layer.getLatLng());
// Set the first as nearest
if (!nearest.marker) {
nearest.marker = layer;
nearest.distance = distance;
// If this marker is nearer, set this marker as nearest
} else if (distance < nearest.distance) {
nearest.marker = layer;
nearest.distance = distance;
}
});
});
Example on Plunker: http://plnkr.co/edit/GDixNNDGqW9rvO4R1dku?p=preview
Now the nearest object will hold the marker that is closest to your drop position. Closest distance may vary according to your zoom level. When you're at zoom level 1, it may look like you've dropped it exactly on the other marker but you could be thousands of miles off. At zoom 18 the difference will be much smaller, but to drop it exactly on the same lat/lng is virtually impossible. Otherwise you could simply compare all the latlng's against the dropped latlng but that won't work in practice.
So now you have the nearest marker and it's distance to the dropped marker you could implement tolerance, something along the lines of: if (nearest.distance < (x / y)) where x is the distance and y the zoomlevel. It's something you'll need to play with to get right. Once you've figured out the correct tolerance you could implement it right along with the distance comparison in the handler.
Good luck, hope this helps

Related

RPG Map questions

I have a map for my Pen and Paper RPG and I want to show it via Leaflet.
I put the Png-File throw a Tile- Making Script and was able to generate this map.
I want to do the following things but don't know how:
Place the equator on the actual equator of the map
Putting bounds on the map, but only for the north-south-axis
The scale calculates with the dimensions of the real earth and i want to give it the dimensions of my world
I want my markers and polygons to repeat every 360°
I would appreciate any help,
Civer
To get the equator you could use a polyline like
var latlngs = [
[0, -180],
[0, 180]
];
var polyline = L.polyline(latlngs, {color: 'red'}).addTo(map);
Not really sure what you mean by "putting bounds on the map". Are you saying you want to limit the user's ability to pan to areas outside of the map? Or are you talking about some sort of visual bounding line?
To do stuff with with different scales I'd suggest you look into how leaflet's Coordinate Reference System works (CRS). Take a look at this page: https://leafletjs.com/examples/crs-simple/crs-simple.html
It looks like you commented out some CRS stuff in your demo.

How can I query the feature that's closest to the geocode result in Mapbox?

I'm trying to create a Mapbox map with thousands of points. I want the user to be able to type an address and a div to be filled with information from the closest point to that address. I'm using the mapbox-gl-geocoder control
What would be perfect is if I could use something like:
geocoder.on('result', function(e) {
var features = map.queryRenderedFeatures(e.result.center)
});
But queryRenderedFeatures doesn't accept latlong coordinates, it requires viewport coordinates.
The next thing I tried was
map.on('moveend', function () {
var xwindow = window.innerWidth / 2;
var ywindow = window.innerHeight / 2;
var xywindow = {'x': xwindow, 'y': ywindow};
var features = map.queryRenderedFeatures(xywindow);
});
That gives results that are near the queried address, but not the closest point. For instance if I geocode an address where I know a point exists, this query will return a different point a few streets away.
Is there a way to use queryRenderedFeatures to get a feature on the location that is returned by the geocode control?
But queryRenderedFeatures doesn't accept latlong coordinates, it requires viewport coordinates.
Yes but you can use map.project that Returns a Point representing pixel coordinates, relative to the map's container, that correspond to the specified geographical location.
Once you have all the features you'll need to get the distance to each of them to find the closest. cheap-ruler would be a good choice for that.
An index like geokdbush probably doesn't make sense here as you're only running it once.

Mapbox: How are tiles sizes and positions calculated

I'm trying to get through the learning curve of not just the mapbox api but how map applications work in general. Currently I'm having difficulty understanding the calculation used in sizing and placing tiles based on LngLat and Zoom level.
I checked out the Slippy maps wiki but it does not seem to align with how mapbox works (or more likely my understanding is incorrect).
I'm hoping someone can point me to a resource that can clearly explain the calculations for the mapbox-gl api tile placement.
Thanks!
More Specifically: I'm trying to figure out how to cover a tile with a 3D plane using threebox. To do this I need to:
get the tile's size (which changes depending on zoom level)
get the tile's position (which I can get using bbox, however I don't think my calculations are correct because at zoom level 2 the 3D plane's latitude is off by 40.97 degrees when placed using threebox)
My calculation for placing the tiles:
var offset = 40.97// temporarily used to fix placement.
var loc_x = bounds[0] + ((bounds[2] - bounds[0])/2); // this works as expected
var loc_y = bounds[1] + offset;
var loc_z = 0;
if (bounds[1] < 0) {
loc_y = bounds[3] - offset;
}
Found the reason I needed the offset property. The 3D plane's registration (0,0 coords) needed to match the tile. By default the 3D plane's registration point was in the center of the mesh, rather than the bottom left.

Leaflet - tooltips for overlapping polylines

Background:
I am working on a web based mapping application for hiking. So the map based on leaflet offers routes on hiking trails that are labeled. As any hiking trail can be part of multiple routes, routes - respectively the corresponding polylines representing the routes - can overlap.
Problem:
Each route has its tooltip (triggered by mouseover, {sticky:true}) showing its label which works as expected for non-overlapping polylines but as soon as two or more routes overlap only the polyline "on top" gets its tooltip opened. This behaviour is not bad per se but as all routes are equally important I would like to show all labels of the routes at the pointer's location (or something like a maximum of 5 labels + x more). I weren't able to find any issue related to this topic.
What I tried:
- Create a feature group for all routes, bind the tooltip to the group, hoping that the tooltip function provides an array of all polylines crossing the pointer's position. As it turned out, I only get information of the polyline on top
- I tried the same with a mousemove event on the map, no success
- Comparing pointer's layerPoint coordinates with all routes' _rings & _parts layPoint arrays to find matching layerPoints, but the success rate is only about 5% as these layerPoints only cover actual points of the polyline but not the connection between two points. Additionally, there is a margin around each polyline that triggers the tolltip before the pointer even touches the polyline (too improve touch action, I guess)
- A solution to the margin problem is to add positive and negative margins to each polyline point before comparing it to the pointer coordinates which improves the outcome but doesn't solve the main problem.
Sidenote:
- All routes are drawn into a single canvas
Long story short, I need external help to accomplish the goal. Maybe some of you have an idea or can provide a solution. Any input is appreciated.
** UPDATE: **
A working but pretty inefficient solution is as follows
Approach:
Calculate the shortest distance from the pointer to all routes in viewport. If distance from the pointer to a route is under a certain threshold, add them to the array of route labels that should be displayed.
Steps:
1.) bind a blank tooltip to the a feature group containing all routes
2.) bind mousemove event to the feature group with the follwing function
var routesFeatureGroup = L.featureGroup(routesGroup)
.bindTooltip('', {sticky: true})
.on('mousemove', function(e){
var routeLabels = [e.layer.options.label]; // add triggering route's label by default
var mouseCoordAbs = el.$map.project(e.latlng);
$.each(vars.objectsInViewport.routes, function(i, v){
if (e.layer.options.id != el.$routes[i].options.id && el.$routes[i]._pxBounds.contains(e.layerPoint)){
var nearestLatlngOnPolyline = getNearestPolylinePoint(e.latlng, el.$routes[i]);
var polyPointCoordAbs = el.$map.project(nearestLatlngOnPolyline);
var distToMouseX = polyPointCoordAbs.x - mouseCoordAbs.x;
var distToMouseY = polyPointCoordAbs.y - mouseCoordAbs.y;
var distToMouse = Math.sqrt(distToMouseX*distToMouseX + distToMouseY*distToMouseY);
if (distToMouse < 15) {
routeLabels.push(el.$routes[i].options.label);
}
}
})
var routesFeatureGroup.setTooltipContent(routeLabels.join('<br>'));
})
Explanation:
I already gather all objects (routes and markers) in the current viewport for another part of the app. All routes currently visible are stored in vars.objectsInViewport.routes (respectively their ids), so I dont have to go through all routes. The layer that triggered the mousemove event is added by default. I then check for each of the routes currently visible if:
- their id is different to the layer that trigger the mousemove event (as this label is added by default)
- if their bounds (in cartesian coordinates: "_pxBounds") contain the cartesian layerPoint of the mousemove event (for a rough approch to exclude routes that don't intersect)
If these conditions are met for a route, calculate the closest latlng point from the pointer to the route. I do this with a custom function, which is a bit to long to post it in this context. (I will if someone asks for it)
The mouse position and the latlng point on the polyline / route are then converted to absolute coordinates using the map-project method
http://leafletjs.com/reference.html#map-project
At last, the distance between these to points is calculated using pythagoras. It is pixel based, so that the zoom level isn't a factor. If the distance is below a certain threshold (15px) they are close enough to the pointer to be considered as being hovered (with the default margins around a polyline), so the label of the route is added to the label array.
Finally the tooltip for the feature group is filled with all labels.
Results are pretty promising even though the operation is pretty expensive. I added a timeout of 50ms to reduce the function call a bit:
var tooltipTimeout;
var routesFeatureGroup = L.featureGroup(routesGroup)
.bindTooltip('', {sticky: true})
.on('mousemove', function(e){
clearTimeout(tooltipTimeout);
tooltipTimeout = setTimeout(function(){
// collect labels
// ...
},50);
.on('mouseout', function(){
clearTimeout(tooltipTimeout);
})
I can give you an idea of how to do this, but I am not 100% sure that it will do the job. There is a plugin for Leaflet (Mapbox) that can tell you if a point is within a Polygon and it returns all the Polygons that contain that point.
If this plugin doesn't work for polylines you can create a polygon from a polyline by just going back from the last point to the first and closing the line (I am not sure if this suits you solution). For example if you have a polyline of connected points of [0, 1, 2, .... n-1, n] you then go back with connecting [n with n-1, n-1 with n-2, ... 1 with 0]. This way you will have the same shape of the polyline but it will be a polygon. This isn't the most optimized solution, it is a quick fix that uses a known and available plugin.
Once you get all the tooltips, you can open all of them at once for each polygon/polyline. Or maybe open some helper tooltip where the user can select which one he wants to open.
I hope this helps! If you figure out a better solution (or find a plugin that does the job) please post it here.

HTML5: dragover(), drop(): how get current x,y coordinates?

How does one determine the (x,y) coordinates while dragging "dragover" and after the drop ("drop") in HTML5 DnD?
I found a webpage that describes x,y for dragover (look at e.offsetX and e.layerX to see if set for various browsers but for the life of me they ARE NOT set).
What do you use for the drop(e) function to find out WHERE in the current drop target it was actually dropped?
I'm writing an online circuit design app and my drop target is large.
Do I need to drop down to mouse-level to find out x,y or does HTML5 provided a higher level abstraction?
The properties clientX and clientY should be set (in modern browsers):
function drag_over(event) {
console.log(event.clientX);
console.log(event.clientY);
event.preventDefault();
return false;
}
function drop(event) {
console.log(event.clientX);
console.log(event.clientY);
event.preventDefault();
return false;
}
Here's a (somewhat) practical example that works in Firefox and Chrome.
To build on top of robertc's answer and work around the issue:
The clientX and clientY are going to be inaccurate proportional to the distance of the cursor from the upper left corner of the element which was dropped.
You may simply calculate the delta distance by subtracting the client coords recorded at dragStart from the client coords recorded at dragEnd. Bear in mind the the element which was dropped still has the exact same position at dragEnd which you may update by the delta distance to get the new left and top positional coordinates.
Here's an demo in react, although the same can be accomplished in plain JS.