Leaflet - Fitbounds and keep center - leaflet

I'm using Leaflet with Mapbox and I'd like to set the view of the map so :
all markers are visible
the center is set to a specific point
It's easy to do each points separately with setView and fitbounds but I don't know how to have both at the same time since setView changes the bounds and fitBounds changes the center. A solution could be to define a center and a zoom but how can I know which zoom will allow all my markers to be visible ?
EDIT
I implemented the solution suggested by IvanSanchez and it works as expected:
let ne=leafletBounds.getNorthEast();
let sw=leafletBounds.getSouthWest();
let neSymetric=[ne.lat + (center.lat - ne.lat)*2, ne.lng + (center.lng - ne.lng)*2];
let swSymetric=[sw.lat +(center.lat - sw.lat)*2, sw.lng + (center.lng - sw.lng)*2];
leafletBounds.extend(L.latLngBounds(swSymetric, neSymetric));

Get your bounds, and create a second L.Bounds instance by applying point symmetry along the centerpoint you want. Create a new L.Bounds containing the original bounds and the symmetric bounds. Run fitBounds() with that.

I found another solution to this. You can simply call map.panTo([lat, lng]) after you've adjusted your map with fitBound(). I am using VueJs and calling this in the mounted() lifecycle hook.
I am fitting bounds so you can see a user's location and also a geographic feature far away, but then I use panTo so the user's real location is at the center of the map after fitBounds. It seems to work seamlessly so far.

Related

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.

Get LngLat form XY

I need to get lngLat coordinates from Mapbox map container center once the map is loaded and during user interaction with it. Tried to do on the basis of Get coordinates of the mouse pointer example but can't realize how to change e.point JSON coordinates from dynamic mouse cursor XY-coordinates to static XY-coordinates of map viewport center (outerWidht/2 and outerHeight/2).
Mapbox.js and MapboxGL both offer getCenter methods that return a map view's geographic center.
You can use various events such as moveend to update the center dynamically.

How to convert pixels into the map coordinate when calling easeTo() in mapbox

I am developing an interactive map in HTML+JavaScript using mapboxgl (0.33.1). When the user clicks a button (which is associated with a particular location in the map), I call easeTo(), which put that location in the center of the map.
window.map.easeTo({
center: item.loc
});
Because my application has some overlapping UI over the bottom half of the map, I actually want to put that location not in the center of the map, but in the center of the top half of the map (25% from the top).
I'd appreciate if somebody could give me a hint how achieve it. My app knows the exact sizes of the window in Pixel (and also the zoom level), but (I assume) I need to convert it into the map-coordinate (from pixel) to add an appropriate offset to the "center" parameter I pass to easyTo() function.
I think I found the answer. I just need to call the project() method -- which was very hard to discover!

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.

MapKit: How Can I Transfer the Exact Same Projection to a New Instance With A Slightly Different Shape?

OK, here's the deal:
I have two views: simple and advanced. On the iPad, they come with a big-ass map view, with a marker that can be moved to indicate a position.
Each view has a different instance of MkMapView. When I switch from one to the other, I want to keep the map at exactly the same position and zoom level, so the user feels as if it is the same map.
However, the shape of the map view is slightly different for each of the views. This is because the advanced search has more stuff above the map.
When I open the map (this is code from an abstract superclass, so both instances get it), I set the region and marker position, like so:
[mapSearchView setRegion:[mapSearchView regionThatFits:[[BMLTAppDelegate getBMLTAppDelegate] searchMapRegion]]];
[myMarker setCoordinate:[[BMLTAppDelegate getBMLTAppDelegate] searchMapMarkerLoc]];
searchMapRegion and searchMapMarkerLoc are static, and reflect the currently displayed map's region and marker location (the center of the map).
Here's the problem:
Because the map is a slightly different shape, there is always a bit of adjusting. This can "bounce" back and forth, so that the map zoom keeps decreasing every time you switch, until you are looking at the whole world.
It doesn't matter whether or not I use regionThatFits. The same thing happens, even with this code:
[mapSearchView setRegion:[[BMLTAppDelegate getBMLTAppDelegate] searchMapRegion]];
[myMarker setCoordinate:[[BMLTAppDelegate getBMLTAppDelegate] searchMapMarkerLoc]];
All I want, is for the exact same zoom and center to be displayed. I don't care is the advanced view cuts a bit off.
How do I get the $##!! MapKit to stop tweaking the zoom factor?
Just FYI. I solved this by creating a custom model layer class that maintains the scale and center point, and is used by multiple MKMapViews. It works pretty well, but the MapKit does sometimes tweak the scale very slightly to fit one of its "detents."