How can I query the feature that's closest to the geocode result in Mapbox? - 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.

Related

Is there a solution like leaflet-knn in ArcGIS (js-api) for finding the nearest point in a geoJSON multi-point feature to a given point?

This is how i get the closest point on a geoJSON line to a given point using leaflet-knn:
Given a point, find the nearest points to it. If the index contains multi-point features, like lines, polygons, and so on, it returns points in those features and can return more than one point in each feature.
const line= L.geoJSON(this.lineFromGeoJson, {
onEachFeature: (feature, layer) => {
layer.bindPopup(
'insignificant feature stuff here',
{maxHeight: this.screenHeight, maxWidth: this.screenWidth, autoPan: true});
}
}).addTo(this.map);
const nearest = leafletKnn(line).nearest(L.latLng(this.lat, this.longt), 1);
Is there anything similar in #arcgis/core?
I'm already using the geodesicUtils.geodesicLengths from arcgis to get the length of this geoJSON line and i figured perhaps there was a solution for this in arcgis that i have not found?
I think you could use geometryEngine.nearestVertices method (ArcGIS JS API - esriGeometry).
Now, this method actually takes a geometry as parameter instead of a layer, so you will probably will need to "create" the geometry using the layer or better a subset of the layer.
You can get a subset of the layer (set of features) with a distance query on the layer using ArcGIS JS API - Query.

How can I set dynamic center point in Mapboxgl?

I am working Mapbox-gl.js (v0.38.0) Ionic3 & angular4. I want to set the dynamic center point. The latitude and longitude should be set based on the data in the real time data place's latitude and longitude.
If I set static center point hard-coded, when my real time data changes it will show the center as per the new markers. So I want to set the center point dynamically.
I tried the following following link,
mapbox example
I have added the real time data and marker. But how can I set the expected center point?
Actually my requirement:
For eg: if the real time data has multiple markers in New York, by default the center point should be pointed to New york. Next time the data may change to Callifornia, that time it should point to California.
Based on the markers my center point should be set.
The following snippet is inspired from zoom-to-linestring example on mapbox-gl
As #Aravind mentions above, we use map.fitBounds, however, we have to determine the bounds, we have to reduce the array of features to determine the LngLatBounds.
Just make sure you loaded the feature data.
Example
let features = [feature, feature] // ... array of features
// first coordinate in features
let co = features[0].geometry.coordinates;
// we want to determine the bounds for the features data
let bounds = features.reduce((bounds, feature) => {
return bounds.extend(feature.geometry.coordinates);
}, new mapboxgl.LngLatBounds(co[0].lng, co[0].lat));
// set bounds according to features
map.fitBounds(bounds, {
padding: 50,
maxZoom: 14.15,
duration: 2000
});
You can use the built-in GeolocateControl to find the location of the user and move the center of the map to that location, Check out this example.
You can use the watchPosition option to enable the tracking of the user's location.
MapBox is actively working to improve the location tracking part. You will have better control over this in the next version, Check out this link.

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.

Leaflet / Mapbox - Drag & 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

How to find if a long/lat point is in the visible bing map

This should be quite simple but I am not getting it.
I have a database of locations with lon/lat specified.
After loading the bing map I get the bounds
var view = map.getBounds();
and then call a webmethod to get all the locations which should be shown (within the bounds of the visible map).
I cannot figure out a query to get the locations (which all have a lon/lat specified) .
This obviously does NOT work as when negative values come into play they mess up the query:
SELECT Location_name, longtitude, latitude FROM location_view WHERE latitude< '40.112' and latitude> '35.783' and longtitude< '28.10453' and longtitude> '19.315'
Is there a normalized way to do this? So the comparison would work?
Your query will work absolutely fine with negative values: a longitude of -130 is still west of a longitude of -120. The only situation in which it won't work is if the bounds of your map crosses the 180th meridian. I.e. the "westmost" longitude is 170 and the "eastmost" latitude is -170.
What database are you using? If you're using SQL Server then you can define each of your locations as a Point using the geography datatype. The geography datatype operates on a round model of the earth, so it will account correctly for crossing the 180th meridian with a query like this:
SELECT Location_name
FROM location_view
WHERE location.STIntersects('POLYGON((19.315 35.783, 28.10453 35.783, 28.10453 40.112, 19.315 40.112, 19.315 35.783))') = 1;